Да, очень простой вопрос на поверхности. Но если вы потратите время, чтобы обдумать это до конца, вы попадете в глубины теории типов неизмеримой. И теория типов смотрит на вас.
Во-первых, конечно, вы уже правильно поняли, что 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
», то есть лямбда-выражения, которое само по себе является родовым c.
Или, альтернативно, мы могли бы go вернуться к более высокой степени родства и более точно объявить тип 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
) - сюрприз-сюрприз, эти места на самом деле мономорфизированы, так что вы, по сути, вернулись в простую c F # -лэнд.
В F #, с другой стороны, подписи типов встречаются редко, в основном только для документации или для. NET взаимодействия. Вне этих двух случаев вы можете написать целую большую сложную программу на F # и ни разу не использовать сигнатуру типа. Вывод типа работает нормально, потому что нет ничего слишком сложного или неоднозначного для него.
И это большое преимущество F # перед Haskell. Да, Haskell позволяет вам express очень сложный материал очень точным способом, это хорошо. Но F # позволяет вам быть очень слабым, почти как Python или Ruby, и при этом компилятор поймает вас, если вы наткнетесь.