Да, очень простой вопрос на поверхности. Но если вы потратите время, чтобы обдумать это до конца, вы попадете в глубины теории типов неизмеримой. И теория типов смотрит также на вас.
Во-первых, конечно, вы уже правильно поняли, что F # не имеет классов типов, и именно поэтому. Но вы предлагаете интерфейс Mappable
. Хорошо, давайте посмотрим на это.
Допустим, мы можем объявить такой интерфейс. Можете себе представить, как будет выглядеть его подпись?
type Mappable =
abstract member map : ('a -> 'b) -> 'f<'a> -> 'f<'b>
Где f
тип, реализующий интерфейс. Ой, подождите! У F # этого тоже нет! Вот f
переменная типа с более высоким родом, а F # вообще не имеет такого рода. Нет способа объявить функцию f : 'm<'a> -> 'm<'b>
или что-то в этом роде.
Но, допустим, мы также преодолели это препятствие. И теперь у нас есть интерфейс , Mappable
который может быть реализован List
, Array
, Seq
и кухонной мойки. Но ждать! Теперь у нас есть метод вместо функции, а методы плохо сочетаются! Давайте посмотрим на добавление 42 к каждому элементу вложенного списка:
// Good ol' functions:
add42 nestedList = nestedList |> List.map (List.map ((+) 42))
// Using an interface:
add42 nestedList = nestedList.map (fun l -> l.map ((+) 42))
Посмотрите: теперь мы должны использовать лямбда-выражение! Нет способа передать эту .map
реализацию другой функции в качестве значения. Фактически конец «функции как ценности» (и да, я знаю, использование лямбды не выглядит очень плохо в этом примере, но поверьте мне, это становится очень уродливым)
Но подождите, мы еще не закончили. Теперь, когда это вызов метода, вывод типа не работает! Поскольку сигнатура типа метода .NET зависит от типа объекта, компилятор не может сделать вывод обоим. На самом деле это очень распространенная проблема, с которой сталкиваются новички при взаимодействии с библиотеками .NET. И единственное лекарство - предоставить подпись типа:
add42 (nestedList : #Mappable) = nestedList.map (fun l -> l.map ((+) 42))
Да, но этого все еще недостаточно! Несмотря на то, что я предоставил подпись для nestedList
себя, я не предоставил подпись для параметра лямбды l
. Какой должна быть такая подпись? Вы бы сказали, что так и должно быть fun (l: #Mappable) -> ...
? О, и теперь мы наконец-то добрались до типов ранга N, как видите, #Mappable
ярлык для «любого типа, 'a
такого что 'a :> Mappable
» - то есть лямбда-выражения, которое само по себе является родовым.
Или, в качестве альтернативы, мы могли бы вернуться к более высокой доброте и объявить тип nestedList
более точно:
add42 (nestedList : 'f<'a<'b>> where 'f :> Mappable, 'a :> Mappable) = ...
Но хорошо, давайте пока отложим вывод типов и вернемся к лямбда-выражению и тому, как мы теперь не можем передавать map
в качестве значения другую функцию. Допустим, мы немного расширили синтаксис, чтобы учесть что-то вроде того, что Elm делает с полями записи:
add42 nestedList = nestedList.map (.map ((+) 42))
Каким бы был тип .map
? Это должен быть ограниченный тип, как в Haskell!
.map : Mappable 'f => ('a -> 'b) -> 'f<'a> -> 'f<'b>
Вау окей Оставляя в стороне тот факт, что .NET даже не позволяет существовать таким типам, фактически мы просто вернули классы типов!
Но есть причина, по которой F # не имеет классов типов. Многие аспекты этой причины описаны выше, но более краткий способ выразить это: простота .
Как видите, это клубок пряжи. Если у вас есть классы типов, у вас должны быть ограничения, более высокая степень родства, ранг N (или, по крайней мере, ранг 2), и прежде чем вы это узнаете, вы запрашиваете нечеткие типы, функции типов, GADT и все остальное.
Но Haskell платит цену за все вкусности. Оказывается, нет хорошего способа вывести все эти вещи. Сорта с более высокими типами работают, но ограничения уже вроде нет. Ранг-N - даже не мечтай об этом. И даже когда это работает, вы получаете ошибки типа, которые вы должны иметь докторскую степень, чтобы понять. И именно поэтому в Haskell вам мягко рекомендуется ставить типовые подписи на всем. Ну, не все - все , но на самом деле почти все. А там, где вы не размещаете сигнатуры типов (например, внутри let
и where
) - сюрприз-сюрприз, эти места на самом деле мономорфизированы, так что вы по сути вернулись в упрощенную F # -лэнд.
В F #, с другой стороны, сигнатуры типов встречаются редко, в основном только для документации или для взаимодействия .NET. Вне этих двух случаев вы можете написать целую большую сложную программу на F # и не использовать сигнатуру типа один раз. Вывод типа работает нормально, потому что нет ничего слишком сложного или неоднозначного для него.
И это большое преимущество F # перед Haskell. Да, Haskell позволяет вам выразить очень сложные вещи очень точно, это хорошо. Но F # позволяет вам быть очень беззаботным, почти как Python или Ruby, и при этом компилятор поймает вас, если вы наткнетесь.