Всем привет! Сегодня, по совету Владимира Матвеева (не могу не упомянуть новый и старый блоги), попытаюсь осветить такую интересную и редкую тему, как F# type functions.

Я думаю многие разработчики, изучающие F# и имеющие опыт разработки на C#, наверняка задавались вопросом: как выразить на F# некоторые встроенные в язык конструкции C#, такие как typeof(T), default(T) и, возможно, гораздо более редко встречающийся оператор sizeof(T)?

Давайте задумается, а что у этих операторов C# есть общего? Все они являются выражениями, параметризируются статически известным типом, не имеют видимых сторонних эффектов и значение результата их вычисления зависит только от явно задаваемого типа-параметра. В принципе, можно было бы заменить все эти операторы обычными generic-методами следующей сигнатуры: Type TypeOf<T>(). Однако раз данная функция чиста и не получает аргументов, то удобнее думать о ней вообще как о значении, параметризованном типом.

Вместо встроенных в язык и грамматику конструкций, F# предлагает в стандартной библиотеке несколько таких значений, обладающих функционалом соответствующих операторов C#:

typeof(List<T>)     =>  typeof<List<T>>
typeof(Action<,,>)  =>  typedefof<Action<_,_,_>>
default(decimal)    =>  Unchecked.defaultof<decimal>
sizeof(int)         =>  sizeof<int>

Стоит отметить, что в C# введён специальный синтаксис для получения generic type definition (открытого generic-типа) – надо вовсе не указывать все типы-параметры, например: Action<> или Dictionary<,>. Однако в F# явные типы-параметры надо указывать всегда (отдельного синтаксиса не предусмотрено), поэтому для получения generic type definition следует пользоваться отдельным значением typedefof<T>, в качестве T указав generic-тип с любыми возможными конкретными типами-параметрами. Иногда можно применить небольшой приём для улучшения читаемости кода: указать в качестве типов-параметров _ – тогда вывод типов автоматически выведет соответствующие типы-параметры в тип obj (как в примере выше).

Так вот, подобные let-привязки значений (то есть не имеющие аргументов), параметризированных типами, в F# называются «type functions». В стандартной библиотеке F# есть ещё несколько примеров type functions, которые Вы наверняка уже замечали:

List.empty
Array.empty
Seq.empty
Map.empty

Можно снова заметить, что данные значения легко представить как чистые generic-функции без аргументов, результат которых зависит только от типа-параметра. Однако следует заметить, что указывать тип-параметр явно для данных значений оказывается совершенно не обязательно (что вовсе невозможно в случае typeof<T> и других рассмотренных выше type functions), тип-параметр может быть неявно определён алгоритмом вывода типов F# по дальнейшему использованию. Получается, что эти type functions представляют собой некие уникальные значения, не зависящее выведенного типа-параметра. По сути, пустой список типа string логически ничем не отличается от пустого списка типа int, так как тип-параметр логически не используется, почему бы в данном случае не иметь удобное полиморфное значение «пустой список», не зависящее от типа-параметра?

Следует добавить, что [] и [||] являются всего лишь синтаксическим сахаром для List.empty и Array.empty соответственно, то есть так же являются type functions.

Возникают вопросы: как определяются свои type functions? Почему для некоторых type functions тип-параметр требуется указывать явно, а для других – нет?

Предупреждаю сразу, что определение собственных type function в F# – очень редкое занятие в программировании на F#. Определить type function очень легко – надо всего лишь добавить явное перечисление типов-параметров для let-привязки значения:

// Список иерархии классов для типа 'T
let typeHierarchy<'T> =
  let rec loop ts (t: System.Type) =
    if t = null then ts
                else loop (t::ts) t.BaseType
  loop [] typeof<'T>

Использование:

typeHierarchy<System.IO.FileStream>
 |> List.map (fun t -> t.Name);;

val it : string list =
  ["Object"; "MarshalByRefObject"; "Stream"; "FileStream"]

Однако следует обратить внимание на то, что явное перечисление типов-аргументов для let-привязок внутри выражений, определений типов и computation expressions запрещено, то есть type functions в F# можно определить только на уровне модуля.

В случае type function подобных typeof<T>, тип возвращаемого значения никак не полагается на тип-параметр type function (typeof<T> является значением обычного типа System.Type), а следовательно, имеет смысл потребовать от пользователя явное указание типа-параметра, иначе вывод типов всегда будет предполагать тип obj. Это можно осуществить, отметив type function специальным атрибутом [<RequiresExplicitTypeArguments>] из стандартной библиотеки F#. Этот атрибут работает для любых let-привязок и методов, отключая вывод типов F#, что помогает избавиться от вывода типа obj для типов-параметров, которые не могут быть корректно выведены из аргументов и должны указываться по месту вызова явно, например:

type Foo =
   member this.ServicesOfType<'T>(name: string) =
     ...

Foo.ServicesOfType() // компилируется, но 'T = obj!

Однако в случае других type functions, таких как Seq.empty и других, тип-параметр может быть успешно выведен по дальнейшему использованию, так как тип возвращаемого значения полагается на тип-параметр (Seq.empty<’a> представляет собой значение типа seq<’a>). Отлично, давайте определим тривиальную type function и попробуем её использовать следующим образом:

let myEmptyList<'a> = List.empty<'a>

let func() =
  let empty = myEmptyList
  (1 :: empty, "a" :: empty) // error

Получаем ошибку следующего содержания:

Type mismatch. Expecting a string list but given a int list. The type ‘string’ does not match the type ‘int’.

То есть значение empty невозможно использовать как полиморфное, как список различного типа в нескольких выражениях. Дело в том, что type значение function по умолчанию не подвергается автоматическому обобщению (automatic generalization) F#, как функции, например:

let func() =
  let empty() = myEmptyList
  (1 :: empty(), "a" :: empty()) // fine

Как раз для решения данной проблемы применяется атрибут [<GeneralizableValue>]. После аннотации type function данным атрибутом, значение начинает рассматриваться как полиморфное и принимать участие в автоматическом обобщении:

[<GeneralizableValue>]
let myEmptyList<'a> = List.empty<'a>

let func() =
  let empty = myEmptyList
  (1 :: empty, "a" :: empty) // fine

Обычно type function следует обязательно отмечать либо атрибутом [<GeneralizableValue>], либо [<RequiresExplicitTypeArguments>], в зависимости от сценария использования.

Напоследок следует сказать, что для компилятора все обращения к type function превращаются в обычный вызов generic-функции, то есть значение type всегда вычисляется заново, что может приводить к забавным эффектам:

let zeroRef<'a> : int ref = ref 0

zeroRef := 1
printfn "%A" zeroRef // {contents = 0;}

Рекомендую определять свои type functions только убедившись, что вычисление не имеет видимых сторонних эффектов и является хорошим претендентом на то, чтобы рассматривать его как значение, параметризованное типом. Ну и понимая, что оно вам действительно надо, конечно.