Сегодня предлагаю обсудить такие элементы F#, как совпадение с образцом (pattern matching) и активные образцы F# (active patterns), которые незаменимы, при работе с цитированием кода (F# quotations). В качестве задания, попробуем написать набор функций, логически похожих на typeof<T> и предназначенных получения различных наследников System.Reflection.MemberInfo для заданных свойств, методов, функций, конструкторов и прочих элементов кода. То есть напишем на F# аналог несуществующего оператора infoof() (читай info-of), аналоги которого все кому не лень, реализуют в C# на базе Expression Trees, например, вот (обсуждение Эрика Липперта здесь).

Давайте попробуем описать простую функцию, получающую процитированное выражение F# и возвращающую объект типа System.Reflection.PropertyInfo в случае, если переданное выражение является выражением доступа к свойству:

open Microsoft.FSharp.Quotations.Patterns

let propertyof expr =
  match expr with
    | PropertyGet(_, info, _) -> info
    | _ -> failwith "Not a property expression"

Всё, что делает данная функция – использует «активный образец» (или «активный шаблон», active pattern) из модуля Microsoft.FSharp.Quotations.Patterns для попытки извлечения выражения доступа к свойству. Функцию легко использовать для получения PropertyInfo статических свойств и свойств различных переменных и литералов, доступных в контексте вызова propertyof:

propertyof<@ (null : string).Length @>

val it : System.Reflection.PropertyInfo =
  Int32 Length {Name = "Length";
                CanRead = true;
                CanWrite = false;
                DeclaringType = System.String;
                PropertyType = System.Int32; ...}

propertyof<@ System.Console.CapsLock @>

val it : System.Reflection.PropertyInfo =
  Boolean CapsLock {Name = "CapsLock";
                    CanRead = true;
                    CanWrite = false;
                    DeclaringType = System.Console;
                    PropertyType = System.Boolean; ...}

Однако функцией будет сложно воспользоваться, если надо будет получить PropertyInfo уровня экземпляра, не имея самого экземпляра класса. Чтобы решить данную проблему, можно позволить помимо выражения доступа к свойству, передавать лямбда-выражение, состоящие из выражения доступа к свойству через параметр лямбды:

<@ fun(s: string) -> s.Length @>

val it : Quotations.Expr<(string -> int)> =
  Lambda (s, PropertyGet (Some (s), Int32 Length, []))

Новая версия функции propertyof принимает вид:

let propertyof expr =
  match expr with
    | PropertyGet(_, info, _) -> info
    | Lambda(arg, PropertyGet(Some(Var var), info, _))
        when arg = var -> info
    | _ -> failwith "Not a property expression"

Тут и раскрывается вся соль совпадения с образцом: шаблоны-образцы могут быть вложены друг в друга, что делает pattern-matching очень мощной техникой, позволяющей легко «опознавать» сложные структуры и конструкции различных объектов. То есть если выражение expr является лямбда-выражением, то параметру лямбда выражение будет дано имя arg, а тело лямбда-выражения будет проверяться на соответствие шаблону PropertyGet(Some(Var var), info, _), который совпадает с выражениями доступа к свойству уровня экземпляра (иначе первый параметр шаблона PropertyGet будет равняться None). Причём экземпляр, к чьему свойству происходит обращение, должен быть задан переменной, совпадающей с шаблоном Var var. Осталось лишь проверить с помощью guard-выражения when идентичность переменной var и аргумента лямбда-выражения arg, тем самым запретив к совпадению лямдба-выражения вида: fun x -> someOtherVar.Property. Вот и всё!

Ок, давайте попробуем ещё один вариант выражения, доступа к свойству необычного литерала (123I – это числовой литерал типа BigInteger в F#):

propertyof<@ 123I.IsZero @>

System.Exception: Not a property expression
   at FSI_0045.propertyof(FSharpExpr expr)
   at <StartupCode$FSI_0049>.$FSI_0049.main@()

Хм, как же на самом деле цитируется данное выражение?

<@ 123I.IsOne @>

val it : Quotations.Expr<bool> =
  Let (copyOfStruct,
     Call (None, BigInteger FromInt32[BigInteger](Int32), [Value 123]),
     PropertyGet (Some copyOfStruct, Boolean IsOne, []))

То есть на самом деле F# создаёт let-биндинг, инициализирует его конструктором BigInteger и затем осуществляет обращение к свойству данного биндинга, то есть выражение 123I.IsOne цитируется как let copyOfStruct = 123I in copyOfStruct.IsOne. Добавим образец, совпадающий и с такими выражениями, функция примет вид:

let propertyof expr =
  match expr with
    | PropertyGet(_, info, _) -> info
    | Lambda(arg, PropertyGet(Some(Var var), info, _))
    | Let(arg, _, PropertyGet(Some(Var var), info, _))
        when arg = var -> info
    | _ -> failwith "Not a property expression"

Обратите внимание, что я объединил два образца через ИЛИ-шаблон | (ещё пример: match x with 1 | 2 | 3 -> true | _ -> false), так как оба образца содержат одинаковый набор имён для совпадений (arg, var, info) соответственно идентичных типов. Обратите внимание, что ограничивающее when-выражение тут действует на оба возможных совпадения ИЛИ-шаблона. Проверяем работоспособность:

[ propertyof<@ System.Console.Out @>
  propertyof<@ (null: Type).IsClass @>
  propertyof<@ "someStringLiteral".Length @>
  propertyof<@ fun(x: string) -> x.Length @> ]

|> List.iter (printfn "%A")

Выводит на экран:

System.IO.TextWriter Out
Boolean IsClass
Int32 Length
Int32 Length

Ок, остановимся на данном варианте и в следующем посте попробуем описать функцию посложнее: methodof.