F# infoof (part 1)
Сегодня предлагаю обсудить такие элементы 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.