Почему в F # так много функций `map` для разных типов?

9

Я учу F #. Я начал FP с Haskell, и мне стало интересно.

Поскольку F # является языком .NET, для меня более разумно объявить интерфейс как Mappable, например, Functorкласс типа haskell .

введите описание изображения здесь

Но, как на рисунке выше, функции F # разделены и реализованы сами по себе. Какова цель дизайна такого дизайна? Для меня введение Mappable.mapи реализация этого для каждого типа данных было бы более удобным.

MyBug18
источник
Этот вопрос не относится к SO. Это не проблема программирования. Я предлагаю вам спросить в F # Slack или на каком-либо другом дискуссионном форуме.
Бент Транберг
5
@BentTranberg Щедро прочитанный, The community is here to help you with specific coding, algorithm, or language problems.будет также охватывать вопросы дизайна языка, если выполняются другие критерии.
Кафер
3
Короче говоря, F # не имеет классов типов и поэтому нуждается в переопределении mapи других общих функциях более высокого порядка для каждого типа коллекции. Интерфейс мало чем поможет, поскольку для каждого типа коллекции требуется отдельная реализация.
dumetrulo

Ответы:

19

Да, очень простой вопрос на поверхности. Но если вы потратите время, чтобы обдумать это до конца, вы попадете в глубины теории типов неизмеримой. И теория типов смотрит также на вас.

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

Федор Сойкин
источник