Чому у F # існує стільки функцій `map` для різних типів


9

Я навчаюсь F #. Я почав FP з Haskell, і мені стало цікаво для цього.

Оскільки F # є .NET мовою, мені здається більш розумним оголосити інтерфейс, як Mappable , як і Functorклас Haskell .

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

Але як на малюнку вище, функції F # відокремлюються та реалізуються самостійно. Яка мета дизайну такого дизайну? Для мене введення Mappable.mapта реалізація цього для кожного типу даних було б зручнішим.


Це питання не належить до SO. Це не проблема програмування. Я пропоную вам запитати у F # Slack або іншому дискусійному форумі.
Bent Tranberg

5
@BentTranberg Щедро прочитане The community is here to help you with specific coding, algorithm, or language problems.також охоплювало б питання мовного дизайну, доки інші критерії виконуються.
kaefer

3
Якщо коротко розповісти, у F # немає класів типів, тому потрібно повторно виконувати mapта інші загальні функції вищого порядку для кожного типу колекції. Інтерфейс мало допоможе, оскільки він все ще потребує кожного типу колекції, щоб забезпечити окрему реалізацію.
dumetrulo

Відповіді:


20

Так, дуже очевидне питання на поверхні. Але якщо ви знайдете час, щоб продумати це до кінця, ви потрапите в глибину теорії типів незмірно. І теорія типів також дивиться на вас.

По-перше, звичайно, ви вже правильно зрозуміли, що у 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 # -land.

У F #, з іншого боку, підписи типів є рідкісними, переважно лише для документації або для .NET interop. Поза цими двома випадками ви можете написати всю велику складну програму в F # і не використовувати один раз підпис типу. Виведення типу працює чудово, тому що немає нічого надто складного або неоднозначного, щоб він міг впоратися.

І це велика перевага F # над Haskell. Так, Haskell дозволяє висловлювати надскладні речі дуже точно, це добре. Але F # дозволяє бути дуже жадібним, майже як Python або Ruby, і все ж змусити компілятор наздогнати вас, якщо ви натрапите.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.