В языке C# существует возможность расширять существующие типы методами расширения, синтаксис вызовов которых похож на вызов экземплярных методов. На этом построен весь LINQ, да и в обычной жизни методам расширения находится полезное применение.
F# также поддерживает возможность расширения существующих типов, но принцип работы и логика этого всего дела несколько иная. В F# не существует таких понятий, как методы расширения, свойства расширения и т.п., вместо этого существует общее понятие под названием "расширение типов" (type extension). Причем под этим термином подразумевает два разных явления: intrinsic extensions ("внутренние расширения") и optional extensions ("необязательные расширения").
И хотя синтаксис расширений совершенно одинаковый, семантика у них принципиально разная. Intrinsic extensions – это аналог частичных типов в C#, а optional extensions – аналог методов расширения, но в более расширенном виде, поскольку мы можем добавить в существующий тип не только «экземплярные» методы, но и статические методы, свойства или события.
Intrinsic extensions a.k.a. частичные типы
Intrinsic extensions представляют собой два отдельных объявления одного типа, в результате чего несколько определений "склеиваются" в один CLR тип.
ПРИМЕЧАНИЕ
Странно, что в официальной документации не проводятся параллели между intrinsic extensions и частичными классами и между optional extensions и методами расширения; ведь с их помощью любой C#/VB разработчик сможет понять эти возможности буквально за пару минут.
При этом у "частичных" типов есть один фатальный существенный недостаток: они должны объявляться в одном и том же файле. В официальной документации к этой возможности говорится, что она отлично подходит для одновременной работы над типом двух разработчиков или при работе с автосгенерированном коде. Мне очень интересно, насколько удобно будет использовать эту возможность в таком контексте, когда оба определения находятся в одном и том же файле:)
Есть подозрение, что такое ограничение связано с тем, что одному файлу исходного класса в языке F# соответствует один модуль, который, в свою очередь преобразуется компилятором F# в статический класс. С другой стороны, в F# существует возможность объявлять типы непосредственно в пространстве имен, и почему в этом случае мы не можем объявлять «частичные» классы в разных файлах, я не знаю!
// SampleType.fs
namespace CustomNamespace
// SampleType объявлен в пространстве имен напрямую, без модуля
type SampleType() =
member public this.foo() = ()
// SampleTypeEx.fs
namespace CustomNamespace
type SampleType with
member public this.boo() = ()
При попытке разнести определение типа по разным файлам вы получите следующую ошибку компиляции: “Namespaces cannot contain extension members except in the same file and namespace where the type is defined. Consider using a module to hold declarations of extension members.”.
ПРИМЕЧАНИЕ
Теперь вы должны понимать, что ждать поддержки дизайнеров для таких средах как Windows Forms или WPF придется довольно долго. Ведь если я все правильно понимаю, то без изменения самого языка, мы можем рассчитывать лишь на возвращение в мир первого .NET-а, когда автосгенерированный код размещался в специализированном регионе в одном файле с остальным кодом.
Но помимо этой особенности, «частичные классы» в F# (a.k.a. intrinsic extensions) обладают рядом ограничений, которые с моей точки зрения выглядят не менее подозрительно.
// "Главное" объявление type SampleType(a: int) =
let f1 = 42
let func() = 42
[<DefaultValue>]
val mutable f2: int
member private x.f3 = 42
static member private f4 = 42
member private this.someMethod() =
printf "a: %d, f1: %d, f2: %d, f3: %d, f4: %d, func(): %d" a f1 this.f2 this.f3 SampleType.f4 (func()) // Вторая "часть" объявления типа type SampleType with member private this.anotherMethod() =
// Следующий код не будет компилироваться! //printf "a: %d" a //printf "f1: %d" f1 //printf "func(): %d" (func())
// Из "частчного" определения у нас есть доступ к членам (members) // через модификаторы "this" или "SampleType" printf "f2: %d, f3: %d, f4: %d" this.f2 this.f3 SampleType.f4
В первом («главном») объявлении класса SampleType мы получаем 3 «значения» и 3 члена: a и f1 становятся закрытыми полями типа int, а func() – закрытой функцией, возвращающей int. Помимо этого мы объявляем еще 3 закрытых члена: f2, f3 и f4, каждый из которых тоже является закрытой переменной. Значения и члены класса в конечном итоге преобразуются компилятором в закрытые поля или закрытые методы и различаются определенными моментами, которые не важны для данного обсуждения.
Интересной особенностью является то, что из «главного» объявления у нас есть доступ ко всем закрытым частям класса, а из «частичного» объявления – только к «членам», но не «значениям»!
По словам Дона Сайма, такое поведение является «фичей», а не «багом»; let binding в типах является достаточно интересным зверем: если объявленное таким образом значение используется в позднее, то оно преобразуется в закрытое поле. В противном случае, оно преобразуется в локальную переменную конструктора. И чтобы упростить анализ того, чем является let binding его область видимости ограничена «лексической областью видимостью» (lexical scope), т.е. «главным определением типа».
Тем не менее, я не вижу особого смысла в текущей реализации intrinsic extensions, поскольку она не применима для дизайнеров, а совместная работа над одним типом двух разработчиков звучит для меня, как ересь, особенно в мире ФП, в котором типы просто не могут быть достаточно большими по определению.
Optional extensions a.k.a. extension methods
С другой стороны, не все так плохо и в отличие от языка C#, F# предоставляет более широкие возможности по расширению существующих типов. Optional extensions позволяют расширять существующие типы методами расширения (статическими и экземплярными), а также свойствами и событиями.
ПРИМЕЧАНИЕ
Еще раз напомню, что синтаксически, разницы между intrinsic extensions («частичными типами») и optional extensions («методами расширения») нет никакой. Разницу определяет компилятор, в зависимости от того, располагается расширение в том же файле или нет.
open System open System.Globalization type Int32 with
// Instance Method Extension member public this.ToHex() =
this.ToString("X")
// Property Extension member public this.Hex with get() = this.ToHex()
// Static Property Extension static member public Infinite with get() = -1
// Static Method Extension static member public FromHex(s: string) =
Int32.Parse(s.Replace("0x", ""), NumberStyles.HexNumber)
printfn "%s" ((32).ToHex()) // 20 printfn "%s" ((32).Hex) // 20 printfn "%d" (Int32.FromHex "0xBeBe") // 48830 printfn "%d" (Int32.Infinite) // -1
Именно за счет этой возможности F# расширяет некоторые стандартные типы BCL, такие как Array дополнительными фишками из мира функционального программирования:
Как правильно написал mstyura, F# использует не методы расширения класса Array; вместо этого используется модуль Array с набором функций. Но в принципе, расширение существующих типов с помощью методов расширения тоже возможно.
open System // Аналог: int[] a = new [] {1, 2, 3} let a = [|1; 2; 3|] // Вызываем "стандартный метод" let ra = Array.AsReadOnly a // Вызываем "статический метод расширения" let fa = Array.filter (fun v -> v > 2) a printfn "Greater than 2: %A" fa // [|3|]
С помощью этой же возможности можно легко добавить ФП-дружественные методы в существующие типы; добавить фабричные методы (аналогичные методу fromHex) или дополнительные свойства.
Ложка дёгтя. Расширение перечислений
Для меня одним из «расширяемых» типов в языке C# всегда были перечисления (enums): добавить простые расчеты, метод получения строкового представления или еще что-нибудь похожее.
Поскольку F# позволяет расширять тип статическими методами или статическими свойствами, то была мысль расширить таким образом некоторые перечисления, в частности BindingFlags:
open System.Reflection type BindingFlags with
// "Свойство" для получения всех экземплярных членов перечисления static member public AllInstanceMembers with get() =
BindingFlags.Public ||| BindingFlags.NonPublic ||| BindingFlags.Instance
Чтобы потом использовать его следующим образом:
let instanceFields = typeof<DateTime>.GetFields(BindingFlags.AllInstance)
Но, к сожалению, при попытке сделать это, мы получим ошибку: “Enumerations cannot have members”.
И опять, такое поведение кажется немного нелогичным. Для меня расширение своих (или существующих) перечислений с помощью методов расширения всегда было полезной возможностью в языке C#. С одной стороны, благодаря размеченным объединениям эта проблема отсутствует для своих собственных перечислений в F#. Но, с другой стороны, бывает же полезно расширить существующие перечисления для упрощения работы с существующими библиотеками.
UPDATE: Решение текущей задачи с помощью модуля BindingFlags (по мотивам комментария mstyura).
Изначально мне показалось очень странным, что F# не очень строг к уникальности имен типов. Это выливается в некоторые неоднозначности с размеченными объединениями и записями, но, с другой стороны, может позволить решить проблему “расширения перечислений”. Вместо расширения существующего перечисления BindingFlags, мы можем просто объявить модуль с таким же именем:
namespace System.Reflection [<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>] [<RequireQualifiedAccess>] module BindingFlags =
let AllInstanceMembers =
BindingFlags.Public |||
BindingFlags.NonPublic |||
BindingFlags.Instance // Теперь используем BindingFlags.AllInstanceMembers let instanceFields = typeof<DateTime>.GetFields(BindingFlags.AllInstanceMembers)
Взаимодействие с языком C#
Методы расширения в C# и расширения типов (optional type extension) в F# являются лишь синтаксическим сахаром, о котором знают компиляторы этих языков. Так, например, метод расширения в C# - это всего лишь статический метод, помеченный атрибутом System.Runtime.CompilerServices.Extension:
[Extension]
public static class StringEx
{
[Extension]
public static bool IsNullOrEmpty(string s)
{
return string.IsNullOrEmpty(s);
}
}
После чего при вызове метода "".IsNullOrEmpty() компилятор будет искать не только экземплярные методы класса String, но и все методы с атрибутом ExtensionAttribute, принимающие String в качестве первого аргумента. Напрямую использовать ExtensionAttribute в языке C# мы не можем и компилятор попросит нас использовать методы расширения, но мы можем воспользоваться этими атрибутами из других языков, чтобы использовать созданные методы в качестве методов расширения из языка C#.
В этом плане, расширения типов в F# построены аналогичным образом, но вместо ExtensionAttribute используется CompilationArgumentCountsAttribute. Поскольку эти два подхода не совместимы, то для использования методов расширения, написанных на F# в других языках (как C#), придется выполнить кое-какие операции вручную:
open System open System.Runtime.CompilerServices [<Extension>] module StringEx =
// Используем тот же синтаксис, что и для расширения типов F# type String with
// Опять же, тот же синтаксис, но явно указываем имя метода [<CompiledName("IsNullOrEmpty")>]
[<Extension>]
member public x.IsNullOrEmpty() =
String.IsNullOrEmpty(x)
В этом случае, мы сможем использовать метод IsNullOrEmpty не только из F#, но и из C# или VB! К сожалению, этот трюк не поможет использовать свойства расширения, написанные на F# в языке C#!
Заключение
На мой неподготовленный взгляд, расширение типов в F# мне показались не слишком продуманным. Частичные типы сейчас вообще кажутся бесполезными, поскольку мы не можем их использовать в разных файлах. Расширения существующих типов (optional extensions) порадовали возможностью расширять типы статическими методами или свойствами. Но не очень порадовало, что этой возможностью нельзя пользоваться с перечислениями, и то, что придется попотеть, чтобы эти расширения можно было использовать в других языках.
По поводу одного файла и 2 определений в нем: м.б. это облегчило или элиминировало бы merge-конфликты, если над типом трудятся 2 программера :D
ОтветитьУдалитьВ этом случае, мы сможем использовать метод IsNullOrEmpty не только из F#, но и из C# или VB! К сожалению, этот трюк не поможет использовать свойства расширения, написанные на F# в языке C#!
ОтветитьУдалитьЯ чего-то не понял? Или ты написал прямо противоположное
@Андрей: :D хз, честно говоря:))
ОтветитьУдалить@eugene: Чтобы использовать методы расширения, написанные на F# в C# или VB нужно добавить руками атрибуты.
Но если на свойства расширения, написанные на F# навесить эти же атрибуты, то это все равно не позволит использовать их из C# или VB.
Вроде так и написал.
Понял в чем прикол. Свойства и методы.
ОтветитьУдалить