Автор: Sergey Teplyakov

В языке 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) порадовали возможностью расширять типы статическими методами или свойствами. Но не очень порадовало, что этой возможностью нельзя пользоваться с перечислениями, и то, что придется попотеть, чтобы эти расширения можно было использовать в других языках.

Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).