Я разрабатываю функциональную библиотеку пользовательских интерфейсов в F #, и я столкнулся с ситуацией, когда мне нужно создать "коллекции" элементов гетерогенных типов. Я не хочу этого делать, прибегая к динамическому программированию и отбрасывая все на obj (технически возможно здесь, особенно потому, что я компилирую Fable). Вместо этого я хочу сохранить как можно больше безопасности типа.
Результирующие типы кажутся немного беспорядочными (особенно по мере увеличения количества значений), но это обеспечивает сильную безопасность типа для моего сценария и будет (в идеале) быть несколько скрытым для пользователей библиотеки.
Но я пытаюсь найти элегантный способ сопоставления шаблонов с этими вложенными типами кортежей, поскольку пользователям библиотеки иногда приходится писать функции по этим значениям. Очевидно, это можно сделать вручную, например,
и компилятор просто вычислит все. Есть ли способ сделать что-то подобное с F #, используя активные шаблоны (или некоторые другие функции)?
Я должен уточнить, что я не пытаюсь сопоставить значения кортежей неизвестной "arity" во время выполнения. Я хочу сделать это только тогда, когда число (и типы) элементов известно во время компиляции. Если бы я делал первое, мне было бы хорошо с динамичным подходом.
Это, наверное, самое лучшее, что можно сделать, и это действительно не так уж плохо для пользователей (просто нужно считать "arity" кортежа, а затем использовать правильный шаблон TupleN). Однако он все еще меня беспокоит, потому что он просто не кажется таким изящным, каким он может быть. Вам не нужно указывать количество элементов при создании x
, почему вы должны это делать, когда сопоставляете его? Мне кажется асимметричным, но я не вижу способа избежать этого.
Есть ли более глубокие причины, почему моя оригинальная идея не будет работать в F # (или статически типизированных языках вообще)? Существуют ли какие-либо функциональные языки там, где это возможно?
Ответ 2
Марк Симан дал вам правильный ответ. Я вместо этого собираюсь сделать что-то совершенно другое, и покажу вам, почему то, что вы пытаетесь сделать с сложными кортежами, на самом деле не работает, даже если вы попробуете подход "жестко закодированные шаблоны для каждого возможного количества элементов" что вам не нравится. Вот несколько попыток реализации вашей идеи, которые не будут работать:
Попытка # 1
Сначала попробуйте написать функцию, которая будет рекурсивно выкидывать все элементы хвоста кортежа, пока не опустится до первой пары, а затем вернет эту пару. Другими словами, что-то вроде List.take 2
. Если это сработает, мы можем применить подобный метод для извлечения других частей сложного кортежа. Но это не сработает, и причина очень поучительна. Здесь функция:
let rec decompose tuple =
match tuple with
| ((a,b),c) -> decompose (a,b)
| (a,b) -> (a,b)
Если я напечатаю эту функцию в хорошей F # IDE (я использую VS-код с плагином Ionide), я увижу красную squiggly под a
в рекурсивном вызове decompose (a,b)
. Это связано с тем, что в этот момент компилятор выбрасывает следующую ошибку:
Type mismatch. Expecting a
'a * 'b
but given a
'a
The resulting type would be infinite when unifying ''a' and ''a * 'b'
Это первый ключ к тому, почему это не сработает. Когда я наводил указатель на tuple
в VS Code, Ionide показывает мне тип, который F # вывел для tuple
:
val tuple : ('a * 'b) * 'b
Подождите, что? Почему a 'b
для последней части составленного кортежа? Разве это не должно быть ('a * 'b) * 'c
? Ну, это из-за следующей строки соответствия:
| ((a,b),c) -> decompose (a,b)
Здесь мы говорим, что аргумент tuple
и его типы должны иметь форму, которая может соответствовать этой строке. Поэтому tuple
должен быть 2-кортежем, так как мы передаем 2-кортеж в качестве параметра decompose
в этом конкретном вызове. И поэтому вторая часть этого 2-кортежа должна соответствовать типу b
, иначе было бы ошибкой типа вызывать decompose
с (a,b)
в качестве параметра. Следовательно, c
в шаблоне (вторая часть 2-кортежа) и b
в шаблоне (вторая часть "внутреннего" 2-кортежа) должны иметь один и тот же тип, и поэтому тип decompose
ограничивается ('a * 'b) * 'b
вместо ('a * 'b) * 'c
.
Если это имеет смысл, тогда мы можем перейти к тому, почему происходит ошибка несоответствия типа. Потому что теперь нам нужно сопоставить часть a
рекурсивного вызова decompose (a,b)
. Поскольку кортеж мы переходим к decompose
must, соответствующему его сигнатуре типа, это означает, что a
должен соответствовать первой части 2-кортежа, и мы уже знаем (поскольку параметр tuple
должен быть способен сопоставить шаблон ((a,b),c)
в инструкции match
, иначе этот оператор не будет компилироваться), что первая часть 2-кортежа сама по себе является еще 2-кортежем типа 'a * 'b
. Правильно?
Ну и что проблема. Мы знаем, что первая часть параметра decompose
должна быть 2-кортежем типа 'a * 'b
. Но шаблон соответствия также ограничивал параметр a
типом 'a
, потому что мы сопоставляем что-то с типом ('a * 'b) * 'b
с ((a,b),c)
. Поэтому одна часть линии заставляет a
иметь тип 'a
, а другая часть заставляет его иметь тип ('a * 'b)
. Эти два типа не могут быть согласованы, и поэтому система типов выдает ошибку компиляции.
Попытка # 2
Но подождите! Как насчет активных шаблонов? Может быть, они могут нас спасти? Ну, позвольте взглянуть на другую вещь, которую я пробовал, что, как я думал, будет работать. И когда это не получилось, это на самом деле научило меня больше о системе типа F #, и почему то, что вы хотите, не будет возможным. Мы поговорим о том, почему в какой-то момент; но сначала здесь код:
let (|Tuple2|_|) t =
match t with
| (a,b) -> Some (a,b)
| _ -> None
let (|Tuple3|_|) t =
match t with
| ((a,b),c) -> Some (a,b,c)
| _ -> None
let (|Tuple4|_|) t =
match t with
| (((a,b),c),d) -> Some (a,b,c,d)
| _ -> None
let (|Tuple5|_|) t =
match t with
| ((((a,b),c),d),e) -> Some (a,b,c,d,e)
| _ -> None
Введите это в свою IDE, и вы увидите обнадеживающий знак. Он компилируется! И если вы наведите указатель на параметр t
в каждом из этих активных шаблонов, вы увидите, что F # определил правильную "форму" для t
в каждом из них. Итак, теперь мы должны сделать что-то вроде этого, верно?
let (%%%) a b = (a,b)
let complicated = 5 %%% "foo" %%% true %%% [1;2;3]
let result =
match complicated with
| Tuple5 (a,b,c,d,e) -> sprintf "5-tuple of (%A,%A,%A,%A,%A)" a b c d e
| Tuple4 (a,b,c,d) -> sprintf "4-tuple of (%A,%A,%A,%A)" a b c d
| Tuple3 (a,b,c) -> sprintf "3-tuple of (%A,%A,%A)" a b c
| Tuple2 (a,b) -> sprintf "2-tuple of (%A,%A)" a b
| _ -> "Not matched"
(Обратите внимание на порядок: поскольку ВСЕ ваши сложные кортежи являются 2-мя кортежами, с сложным кортежем в качестве первой части 2-кортежа, шаблон Tuple2
будет соответствовать любому такому кортежу, если он был первым.)
Это кажется многообещающим, но это также не сработает. Введите (или вставьте) это в свою среду IDE, и вы увидите красное squiggly под шаблоном Tuple5 (a,b,c,d,e)
(первый шаблон оператора match
). Я скажу вам, что ошибка за минуту, но сначала наведите курсор на определение complicated
и убедитесь, что оно исправлено:
val complicated : ((int * string) * bool) * int list
Да, это выглядит правильно. Поэтому, поскольку это не может соответствовать активному шаблону Tuple5
, почему этот активный шаблон просто не возвращает None
и позволяет перейти к шаблону Tuple4
(который будет работать)? Ну, посмотрим на ошибку:
Type mismatch. Expecting a
((int * string) * bool) * int list -> 'a option
but given a
((('b * 'c) * 'd) * 'e) * 'f -> ('b * 'c * 'd * 'e * 'f) option
The type 'int' does not match the type ''a * 'b'
Там нет 'a
в любом из двух несоответствующих типов. Откуда появился 'a
? Ну, если вы специально наводите курсор на слово Tuple5
в этой строке, вы увидите подпись типа Tuple5
:
active recognizer Tuple5: ((('a * 'b) * 'c) * 'd) * 'e -> ('a * 'b * 'c * 'd * 'e) option
То, откуда пришел 'a
. Но что более важно, сообщение об ошибке сообщает вам, что первая часть complicated
, int
не может соответствовать 2-кортежу. Зачем это пытаться? Опять же, поскольку match
выражения должны соответствовать типу вещи, которую они соответствуют, и поэтому они ограничивают этот тип. Так же, как мы видели с помощью функции decompose
, это происходит и здесь. Вы можете увидеть это лучше, изменив переменную let result
в функцию, например:
let showArity t =
match t with
| Tuple5 (a,b,c,d,e) -> sprintf "5-tuple of (%A,%A,%A,%A,%A)" a b c d e
| Tuple4 (a,b,c,d) -> sprintf "4-tuple of (%A,%A,%A,%A)" a b c d
| Tuple3 (a,b,c) -> sprintf "3-tuple of (%A,%A,%A)" a b c
| Tuple2 (a,b) -> sprintf "2-tuple of (%A,%A)" a b
| _ -> "Not matched"
showArity complicated
Теперь функция showArity
компилируется без ошибок; у вас может возникнуть соблазн радоваться, но вы увидите, что его нельзя вызвать с помощью значения complicated
, которое мы определили ранее, и что вы получаете ошибку несоответствия типа (где, в конечном счете, int
не может совпадать 'a * 'b
). Но почему showArity
компилируется без ошибок? Ну, наведите указатель на тип своего аргумента t
:
val t : ((('a * 'b) * 'c) * 'd) * 'e
Итак, t
был ограничен тем, что я назову "сложным 5-кортежем" (который на самом деле по-прежнему остается только 2-кортежем, помните) с помощью этого первого шаблона Tuple5
. И другие шаблоны Tuple4
, Tuple3
и Tuple2
будут соответствовать, потому что на самом деле они фактически соответствуют 2-кортежам в реальности. Чтобы показать это, удалите строку Tuple5
из функции showArity
и посмотрите на ее результат при запуске showArity complicated
в F # Interactive (вам также нужно будет заново запустить определение showArity
). Вы получите:
"4-tuple of (5,"foo",true,[1; 2; 3])"
Выглядит хорошо, но подождите: теперь удалите строку Tuple4
и запустите определение showArity
, а также строку showArity complicated
. На этот раз он производит:
"3-tuple of ((5, "foo"),true,[1; 2; 3])"
Посмотрите, как это согласовано, но не разложил "самый внутренний" кортеж (int * string
)? Вот почему вам нужен порядок. Запустите его еще раз, оставив только оставшуюся строку Tuple2
, и вы получите:
"2-tuple of (((5, "foo"), true),[1; 2; 3])"
Таким образом, этот подход не будет работать: вы не можете определить "фальшивую сущность" сложного кортежа. ( "Поддельная арность" в цитатах с испугом, потому что арность всех этих кортежей действительно 2, но мы пытаемся рассматривать их так, как если бы они были 3- или 4- или 5-кортежи). Поскольку любой шаблон, чья "фальшивая арность" меньше, чем у сложного кортежа, который вы передаете, будет по-прежнему соответствовать, но он не будет разлагать какую-то часть сложного кортежа. Хотя любой шаблон, чья "фальшивая арность" больше, чем у сложного кортежа, который вы передаете ему, просто не будет компилировать, так как он создает несоответствие типа между самой внутренней частью кортежа, re сопоставлено с.
Я надеюсь, что прочтение всего этого дало вам лучшее понимание тонкостей системы типа F #; Я знаю, что писать его, конечно, многому научил.