Можно ли передавать размеченные теги объединения в качестве аргументов?

Можно ли передать тип размеченного тега объединения другой функции, чтобы она могла использовать его для сопоставления с образцом?

Нерабочий пример того, что я имею в виду:

type Animal = Pig of string | Cow of string | Fish of string

let animals = [Pig "Mike"; Pig "Sarah"; Fish "Eve"; Cow "Laura"; Pig "John"]

let rec filterAnimals animalType animals =
    if animals = [] then
        []
    else
        let rest = filterAnimals animalType (List.tail animals)
        match List.head animals with
        |animalType animal -> animal::rest // <- this doesn't work
        |_ -> rest

printfn "%A" (filterAnimals Pig animals)

person monoceres    schedule 26.03.2014    source источник
comment
Не связанный с вашим вопросом, но предположим, что ваш пример работает, вы можете объединить все сопоставления с шаблоном в один: let rec filterAnimals animalType = function | [] -> [] | animalType animal :: rest -> animal::(filterAnimals animalType rest) | _ :: rest -> rest.   -  person Søren Debois    schedule 26.03.2014


Ответы (5)


Дискриминированные союзы работают лучше всего, если между падежами нет семантического совпадения.

В вашем примере каждый случай содержит один и тот же компонент с одинаковым значением, string указывающий «название животного». Но это семантическое совпадение! Затем дискриминационный союз заставит вас проводить различия, которых вы не хотите: вы не хотите, чтобы вас заставляли различать «имя свиньи» и «имя коровы». ; вы просто хотите думать о «названии животного».

Давайте сделаем тип, который подходит лучше:

type Animal = Pig  | Cow  | Fish
type Pet = Animal * string

let animals = [(Pig, "Mike"); (Fish, "Eve"); (Pig, "Romeo")

С этим типом фильтрация не-Pigs является однострочной:

animals |> List.filter (fst >> (=) Pig) 

Если не у каждого животного есть имя, используйте вариант типа:

type Pet = Animal * string option

Вы использовали бы размеченный союз для своих животных, если бы знали, что, скажем, у каждого Pig есть имя, а у Fish нет: эти случаи не пересекаются.

person Søren Debois    schedule 26.03.2014
comment
Очень полезные комментарии! Я только начинаю изучать F# и FP в целом, и мое представление о типе несколько испорчено годами, проведенными в ООП. - person monoceres; 26.03.2014
comment
@ Сорен Дебуа, что, если у свиней и коров есть имена, а у рыб нет? Как бы я это смоделировал? - person Fyodor Soikin; 17.04.2015
comment
@Федор Сойкин Неловко. Вы можете ввести промежуточный тип NamedAnimal; вы могли бы отказаться от размеченных союзов и вместо этого использовать классы и наследование; или, если вам повезет, тот факт, что именно рыбы не имеют имен, не имеет значения для вашего приложения, и вы можете придерживаться варианта, предложенного выше. - person Søren Debois; 17.04.2015

Вы можете изменить функцию filterAnimals, чтобы она принимала в качестве входных данных частично активный шаблон:

let rec filterAnimals (|IsAnimalType|_|) animals =
    if animals = [] then
        []
    else
        let rest = filterAnimals (|IsAnimalType|_|) (List.tail animals)
        match List.head animals with
        | IsAnimalType animal -> animal::rest
        | _ -> rest

Затем вы можете определить активный частичный шаблон для свиней:

let (|IsPig|_|) candidate =
    match candidate with
    | Pig(_) -> Some candidate
    | _ -> None

И вы можете вызвать функцию следующим образом (пример FSI):

> filterAnimals (|IsPig|_|) animals;;
val it : Animal list = [Pig "Mike"; Pig "Sarah"; Pig "John"]

На самом деле вы можете уменьшить Partial Active Pattern следующим образом:

let (|IsPig|_|) = function | Pig(x) -> Some(Pig(x)) | _ -> None

И оказывается, что их можно даже инлайнить:

> filterAnimals (function | Pig(x) -> Some(Pig(x)) | _ -> None) animals;;
val it : Animal list = [Pig "Mike"; Pig "Sarah"; Pig "John"]
> filterAnimals (function | Fish(x) -> Some(Fish(x)) | _ -> None) animals;;
val it : Animal list = [Fish "Eve"]
> filterAnimals (function | Cow(x) -> Some(Cow(x)) | _ -> None) animals;;
val it : Animal list = [Cow "Laura"]
person Mark Seemann    schedule 26.03.2014
comment
Это работает, однако меня беспокоит, что мне придется писать шаблоны для всех моих разных животных (кто знает, их могут быть сотни!). Я все еще изучаю F#, так что, возможно, я злоупотребляю системой типов, но меня расстраивает то, что я не могу создавать вспомогательные функции для обработки своих объединений. - person monoceres; 26.03.2014
comment
Если у вас есть сотни разных животных, я думаю, можно с уверенностью сказать, что вы не должны моделировать их как Дискриминированный союз с сотнями случаев... - person Mark Seemann; 26.03.2014
comment
В моем случае это на самом деле токены для языка программирования (я пишу парсер на F# в качестве учебного проекта). Необходимость создавать функции IsRightParen, IsLeftParen, IsKeyword, когда F# уже знает типы, выглядит не очень элегантно. - person monoceres; 26.03.2014
comment
@monoceres Кстати, если вы занимаетесь токенизацией и синтаксическим анализом, я настоятельно рекомендую вам проверить FParsec. Это очень мощно и элегантно. - person Jwosty; 27.03.2014
comment
Это обман! На самом деле я пытаюсь изучить F#/Функциональное программирование, реализуя синтаксический анализатор :) - person monoceres; 27.03.2014

Просто для полноты картины я перечислю это решение.
Если вы процитировав ваш вклад, вы сможете рассуждать о названиях тегов:

open Microsoft.FSharp.Quotations

type Animal = Pig of string | Cow of string | Fish of string

let isAnimal (animalType : Expr) (animal : Expr) =
    match animal with
        | NewUnionCase(at, _) ->
            match animalType with
                | Lambda (_, NewUnionCase (aatt, _)) -> aatt.Name = at.Name
                | _ -> false
        | _ -> false

let animal = <@ Pig "Mike" @>
isAnimal <@ Pig @> animal  // True
isAnimal <@ Cow @> animal  // False

Это, по общему признанию, довольно многословно, и это стало бы еще больше, если бы вы хотели процитировать список вместо одного значения.

Немного другая версия, где мы указываем только тип животного, позволит вам легко фильтровать списки животных, как вам нужно (ценой некоторого сомнительного сравнения строк):

open Microsoft.FSharp.Quotations

type Animal = Pig of string | Cow of string | Fish of string

let isAnimal (animalType : Expr) animal =
    match animalType with
        | Lambda (_, NewUnionCase (aatt, _)) -> animal.ToString().EndsWith(aatt.Name)
        | _ -> false

let animal = Pig "Mike"  // No quote now, so we can process Animal lists easily
isAnimal <@ Pig @> animal  // True
isAnimal <@ Cow @> animal  // False

let animals = [Pig "Mike"; Pig "Sarah"; Fish "Eve"; Cow "Laura"; Pig "John"]
let pigs = animals |> List.filter (isAnimal <@ Pig @>)

Последняя версия на самом деле не лучше, чем передача имени тега в виде строки.

person Mau    schedule 26.03.2014

Нет, невозможно передать только тег, чтобы иметь возможность отдельно обрабатывать тег и строку, вы можете определить их следующим образом:

type AnimalType = Pig  | Cow  | Fish
type Animal = Animal of AnimalType * string

let animals = [Animal (Pig, "Mike"); Animal (Pig, "Sarah"); Animal (Fish, "Eve"); Animal (Cow, "Laura"); Animal (Pig, "John")]

let rec filterAnimals animalType animals =
    if animals = [] then
        []
    else
        let rest = filterAnimals animalType (List.tail animals)
        match List.head animals with
        | Animal (x, animal) when x = animalType -> animal::restwork
        |_ -> rest

printfn "%A" (filterAnimals Pig animals)

В качестве альтернативы вы можете использовать только кортеж AnimalType * string

ОБНОВЛЕНИЕ

Что касается вашего вопроса в комментариях о том, что произойдет, если структура не всегда одинакова, вы можете использовать хитрость: вы можете сравнить тип двух размеченных союзов, поскольку каждый тег компилируется в другой подкласс.

type Animal = 
    | Person of string * string 
    | Pig of string 
    | Cow of string 
    | Fish of string

let animals = [Pig "Mike"; Pig "Sarah"; Fish "Eve"; Cow "Laura"; Pig "John"]

let rec filterAnimals animalType animals =
    if animals = [] then
        []
    else
        let rest = filterAnimals animalType (List.tail animals)
        match List.head animals with
        | x when animalType.GetType() = x.GetType() -> x::rest
        |_ -> rest

printfn "%A" (filterAnimals (Pig "") animals)

Но прежде чем пойти по этому пути, вы должны подумать, действительно ли вам нужно моделировать вашу проблему таким образом.

Даже если вы решите использовать эту структуру, я бы предпочел использовать встроенную функцию фильтра, см. Решение, предложенное @polkduran.

person Gus    schedule 26.03.2014
comment
Что, если мне нужно, чтобы у разных животных были разные типы данных? Например, у людей есть и имя, и фамилия. - person monoceres; 26.03.2014
comment
Вам нужно будет написать больше кода, но если вы хотите, чтобы функция принимала только TAG, вам нужно определить его отдельно. - person Gus; 26.03.2014
comment
Да я вижу. Тогда подходит ли какой-либо другой тип, кроме разграниченного союза? - person monoceres; 26.03.2014
comment
Как насчет типа Animal = Human строки * string | Животное AnimalType * строка - person Gus; 26.03.2014
comment
Вы также можете определить DU для включения строки и строки * строки, но я знаю, что вы потеряете ограничение, согласно которому Person должен быть строкой * строкой. Есть трюк, который вы можете использовать, зная, что DU скомпилирован в классы, смотрите обновление. - person Gus; 26.03.2014

Это не отвечает на ваш вопрос напрямую, но предлагает альтернативный способ добиться того, чего вы хотите. Вы можете отфильтровать свой список, используя существующую функцию высокого порядка List.filter и сопоставление с образцом:

let pigs = animals |> List.filter (function |Pig(_)-> true |_->false ) 

Я думаю, что это более идиоматический подход: вы фильтруете свой список, используя шаблон, в этом случае вы фильтруете своих животных, оставляя только тех, кто удовлетворяет шаблону Pig(_).

person polkduran    schedule 26.03.2014