Так, дуже очевидне питання на поверхні. Але якщо ви знайдете час, щоб продумати це до кінця, ви потрапите в глибину теорії типів незмірно. І теорія типів також дивиться на вас.
По-перше, звичайно, ви вже правильно зрозуміли, що у 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, і все ж змусити компілятор наздогнати вас, якщо ви натрапите.