Можно ли ссылаться на предполагаемые типы или извлекать типы аргументов функций в F #? - PullRequest
0 голосов
/ 23 января 2019

Для контекста, я играю с шаблоном внедрения частичной зависимости, описанным в https://fsharpforfunandprofit.com/posts/dependency-injection-1/.

Если я хочу передать функцию другой функции (скажем, в корне композиции для DI), было бы полезно иметь возможность дать движку типа FSharp некоторые подсказки о том, что я пытаюсь сделать, иначе вроде взрывается в бесполезный беспорядок, пока все не заработает. Для этого я хотел бы сослаться на «типы частично примененных версий существующих функций»

Например, скажем, у меня есть настройки

let _getUser logger db userId =
  logger.log("getting user")
  User.fromDB (db.get userId)

let _emailUserId sendEmail getUser emailContents userId =
  let user = getUser userId
  do sendEmail emailContents user.email

// composition root
let emailUserId =
  let db = Db()
  let logger = Logger()
  let sendEmail = EmailService.sendEmail
  let getUser = _getUser logger db
  _emailUserId sendEmail getUser

Я хочу предоставить подсказки типа _emailUserId, например

let _emailUserId
  // fake syntax. is this possible?
  (sendEmail: typeof<EmailService.sendEmail>)
  // obviously fake syntax, but do I have any way of defining
  // `type partially_applied = ???` ?
  (getUser: partially_applied<2, typeof<_getUser>>)
  emailContents userId
  = 
  ...

потому что в противном случае моя IDE очень мало помогает при написании _emailUserId.

Вопрос

(добавлено явно, потому что скрывать его в блоке кода немного вводило в заблуждение).

Позволяет ли система типов F # каким-либо образом ссылаться на существующие логические типы или основываться на них?

Например type t = typeof<some inferred function without a manually written type signature>

И может ли система типов F # позволить мне выразить частично примененную функцию, не записывая вручную ее типы аргументов?

E..g type partial1<'a when 'a is 'x -> 'y -> 'z> = 'y -> 'z>, может использоваться как partial1<typeof<some ineferred function without a manually written signature>?

Хотя я все еще благодарен за отзывы о шаблоне, который я использую, это не основной вопрос, а только контекст. На мой взгляд, этот вопрос вполне применим к разработке на F #.

Единственный способ получить то, что я хочу, - это жестко закодировать полные типы функций:

let _emailUserId
  (sendEmail: string -> string -> Result)
  (getUser: string -> User)
  emailContents userId
  = 
  ...

, что приводит к дублированию подписей, в значительной степени сводит на нет преимущества великолепной системы логического вывода F #, и его довольно сложно поддерживать при расширении этого шаблона за пределы игрушечного примера StackOverflow.

Мне удивительно, что это неочевидно или не поддерживается - мой другой опыт работы с системой с богатыми типами - это Typescript, где такого рода вещи довольно легко сделать во многих случаях с помощью встроенного синтаксиса.

Ответы [ 2 ]

0 голосов
/ 23 января 2019

В F # очень легко создавать простые типы.Впоследствии вместо примитивных типов, таких как строки, целые числа и т. Д., Часто лучше построить «дерево», состоящее из записей / различающихся объединений, и хранить только примитивные типы в самом низу.В вашем примере, я думаю, что я бы просто передал одну запись функций «корню композиции», например, я бы сделал что-то вроде этого:

type User =
    {
        userId : int
        userName : string
        // add more here
    }

type EmailAttachment =
    {
        name : string
        // add more to describe email attachment, like type, binary content, etc...
    }

type Email =
    {
        address : string
        subject : string option
        content : string
        attachments : list<EmailAttachment>
    }

type EmailReason =
    | LicenseExpired
    | OverTheLimit
    // add whatever is needed


type EmailResult =
    | Sucess
    | Error of string

type EmailProcessorInfo =
    {
        getUser : string -> User
        getEmail : EmailReason -> User -> Email
        sendMail : Email -> EmailResult
    }

type EmailProcessor (info : EmailProcessorInfo) =
    let sendEmailToUserIdImpl userId reason =
        info.getUser userId
        |> info.getEmail reason
        |> info.sendMail

    member p.sendEmailToUserId = sendEmailToUserIdImpl
    // add more members if needed.

EmailProcessor - это ваш _emailUserId.Обратите внимание, что я передаю |> результат предыдущего вычисления следующему в sendEmailToUserIdImpl.Это объясняет выбор подписей.

Вам не нужно создавать EmailProcessor класс и, фактически, если он имеет только один член, то, вероятно, лучше оставить его в функции, например

let sendEmailToUserIdImpl (info : EmailProcessorInfo) userId reason =
    info.getUser userId
    |> info.getEmail reason
    |> info.sendMail

Однако, если в итоге получится несколько членов, основанных на одном и том же info, то есть несколько преимуществ использования класса.

В качестве заключительного замечания.Ссылка, которую вы используете в своем вопросе, великолепна, и я часто обращаюсь к ней, когда застреваю в чем-то.Он покрывает множество крайних случаев и показывает различные варианты, чтобы делать похожие вещи.Однако, если вы только начинаете изучать F #, то некоторые концепции могут быть трудными для понимания или даже не нужны для простых случаев, таких как рассмотренный вами.Простого типа параметра может быть достаточно, чтобы различать случаи, когда вы можете / не можете создать какой-либо объект, и поэтому можно избежать использования полностью продуманного вычислительного выражения ResultBuilder.В F # обширное ведение журнала (которое в основном необходимо в C #) на самом деле не требуется из-за отсутствия нулей (если оно правильно спроектировано), гораздо меньшего числа исключений, расширенного сопоставления с образцом и т. Д. И т. Д. И т. Д.

0 голосов
/ 23 января 2019

Вы можете определить псевдонимы типов, которые в основном являются новыми именами для существующих типов:

type SendMail = string -> string -> Result
type GetUser = string -> User

Вы можете использовать их везде, где вам нужно / нужно, чтобы давать подсказки типу компилятору, но они будут не будут новыми типами или подтипами, и сам компилятор все равно выведет исходный тип.И вы можете определить их для любого типа;приведенные выше предназначены для сигнатур функций, потому что это ваш пример, но они также могут быть для типов записей или классов .NET.

Для параметров функции вы можете использовать их следующим образом:

let _emailUserId (sendMail : SendMail) (getUser : GetUser) emailContents userId =
/* .... */
...