F # возможно ли «преобразовать» значение различенного объединения в «суперсет»? - PullRequest
1 голос
/ 14 июня 2019

Допустим, есть два союза, где один является строгим подмножеством другого.

type Superset =
| A of int
| B of string
| C of decimal

type Subset =
| A of int
| B of string

Возможно ли автоматическое преобразование значения подмножества в значение надмножества , не прибегая к явному сопоставлению с образцом? Например:

let x : Subset = A 1
let y : Superset = x // this won't compile :(

Также идеально, если тип подмножества был изменен, чтобы он больше не был подмножеством, тогда компилятор должен пожаловаться:

type Subset =
| A of int
| B of string
| D of bool // - no longer a subset of Superset!

Я считаю, что это невозможно, но все же стоит спросить (хотя бы понять, почему это невозможно)

ПОЧЕМУ ЭТО НУЖНО

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

1 Ответ

3 голосов
/ 14 июня 2019

Извините, нет

Извините, но это невозможно.Взгляните на https://fsharpforfunandprofit.com/posts/fsharp-decompiled/#unions - вы увидите, что F # компилирует разграниченные объединения в классы .NET, каждый из которых отделен друг от друга без общих предков (конечно, кроме Object).Компилятор не пытается идентифицировать подмножества или надмножества между различными DU.Если бы это работало так, как вы предлагали, это было бы серьезным изменением, потому что единственный способ сделать это - сделать подмножество DU базовым классом, а класс надмножества - его производным классом с дополнительным свойством.И это приведет к следующему поведению изменения кода:

type PhoneNumber =
| Valid of string
| Invalid

type EmailAddress =
| Valid of string
| ValidButOutdated of string
| Invalid

let identifyContactInfo (info : obj) =
    // This came from external code we don't control, but it should be contact info
    match (unbox obj) with
    | :? PhoneNumber as phone -> // Do something
    | :? EmailAddress as email -> // Do something

Да, это плохой код, и его следует писать по-другому, но это иллюстрирует суть.При текущем поведении компилятора, если identifyContactInfo получит объект EmailAddress, тест :? PhoneNumber не будет выполнен, и он войдет во вторую ветвь совпадения и будет рассматривать этот объект (правильно) как адрес электронной почты.Если бы компилятор угадывал надмножества / подмножества на основе имен DU, как вы предлагаете здесь, то PhoneNumber будет считаться подмножеством EmailAddress и, следовательно, станет его базовым классом.И затем, когда эта функция получит объект EmailAddress, тест :? PhoneNumber будет успешным (поскольку экземпляр производного класса всегда может быть приведен к типу его базового класса).И тогда код будет вводить ветку first выражения соответствия, и ваш код может затем попытаться отправить текстовое сообщение на адрес электронной почты.

Но подождите ...

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

type AorB =
| A of int
| B of string

type ABC =
| AorB of AorB
| C of decimal

type ABD =
| AorB of AorB
| D of bool

Тогда ваши выражения соответствия для ABC могут выглядеть следующим образом:

match foo with
| AorB (A num) -> printfn "%d" num
| AorB (B s) -> printfn "%s" s
| C num -> printfn "%M" num

И если вам нужно передать данные между ABC и ABD:

let (bar : ABD option) =
    match foo with
    | AorB data -> Some (AorB data)
    | C _ -> None

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...