Расширение типа для размеченного объединения в F #

Я определил следующий размеченный союз:

type Expr = 
    | Con of Num
    | Var of Name
    | Add of Expr * Expr
    | Sub of Expr * Expr
    | Mult of Expr * Expr
    | Div of Expr * Expr
    | Pow of Expr * Expr

Затем я создал функцию красивой печати следующим образом:

let rec stringify expr =
    match expr with
    | Con(x) -> string x
    | Var(x) -> string x
    | Add(x, y) -> sprintf "(%s + %s)" (stringify x) (stringify y)
    | Sub(x, y) -> sprintf "(%s - %s)" (stringify x) (stringify y)
    | Mult(x, y) -> sprintf "(%s * %s)" (stringify x) (stringify y)
    | Div(x, y) -> sprintf "(%s / %s)" (stringify x) (stringify y)
    | Pow(x, y) -> sprintf "(%s ** %s)" (stringify x) (stringify y)

Теперь я хочу, чтобы мой тип Expr использовал эту функцию для своего ToString() метода. Например:

type Expr = 
    | Con of Num
    | Var of Name
    | Add of Expr * Expr
    | Sub of Expr * Expr
    | Mult of Expr * Expr
    | Div of Expr * Expr
    | Pow of Expr * Expr
    override this.ToString() = stringify this

Но я не могу этого сделать, потому что stringify еще не определен. Ответ состоит в том, чтобы определить Stringify как член Expr, но я не хочу загрязнять свое первоначальное объявление типа этим специализированным методом, который со временем будет расти. Поэтому я решил использовать абстрактный метод, который я мог бы реализовать с помощью расширения внутреннего типа ниже. в файле. Вот что я сделал:

type Expr = 
    | Con of Num
    | Var of Name
    | Add of Expr * Expr
    | Sub of Expr * Expr
    | Mult of Expr * Expr
    | Div of Expr * Expr
    | Pow of Expr * Expr
    override this.ToString() = this.Stringify()
    abstract member Stringify : unit -> string

Но я получаю следующую ошибку компилятора:

ошибка FS0912: этот элемент объявления не разрешен в дополнении

Сообщение даже не кажется правильным (я еще не создаю расширение типа), но я понимаю, почему он жалуется. Он не хочет, чтобы я создавал абстрактный член для размеченного типа объединения, потому что он не может быть унаследован. Несмотря на то, что я действительно не хочу наследования, я хочу, чтобы он вел себя как частичный класс в C #, где я могу закончить определение его в другом месте (в данном случае в том же файле).

Я закончил "мошенничеством", используя силу позднего связывания атрибута StructuredFormatDisplay вместе с sprintf:

[<StructuredFormatDisplay("{DisplayValue}")>]
type Expr = 
    | Con of Num
    | Var of Name
    | Add of Expr * Expr
    | Sub of Expr * Expr
    | Mult of Expr * Expr
    | Div of Expr * Expr
    | Pow of Expr * Expr
    override this.ToString() = sprintf "%A" this

/* stringify function goes here */

type Expr with
    member public this.DisplayValue = stringify this

Хотя теперь и sprintf, и ToString выводят одну и ту же строку, и нет никакого способа получить результат Add (Con 2,Con 3) в отличие от (2 + 3), если я этого хочу.

Так есть ли другой способ сделать то, что я пытаюсь сделать?

P.S. Я также заметил, что если я помещаю атрибут StructuredFormatDisplay в расширение вместо исходного типа, он не работает. Мне такое поведение не кажется правильным. Кажется, что либо компилятор F # должен добавить атрибут к определению типа, либо запретить использование атрибутов при увеличении типа.


person luksan    schedule 03.08.2013    source источник


Ответы (3)


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

Однако, поскольку вы предпочитаете, чтобы ваш тип DU был ясным, подумайте о том, чтобы обернуть тип данных в одинарный DU:

// precede this with your definitions of Expr and stringify
type ExprWrapper = InnerExpr of Expr with
    static member Make (x: Expr) = InnerExpr x
    override this.ToString() = match this with | InnerExpr x -> stringify x

// usage
let x01 = Add(Con 5, Con 42) |> ExprWrapper.Make
printfn "%O" x01
// outputs: (5 + 42)
printfn "%s" (x01.ToString())
// outputs: (5 + 42)
printfn "%A" x01
// outputs: InnerExpr(Add (Con 5,Con 42))

Цитата из этого ответа:

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

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

person bytebuster    schedule 04.08.2013
comment
Я думаю, что приму это, даже если я не могу его использовать, потому что это, по крайней мере, метод, который я не рассматривал. - person luksan; 06.08.2013

Вы рассматривали возможность определения своего ToString в дополнении?

type Num = int
type Name = string

type Expr = 
    | Con of Num
    | Var of Name
    | Add of Expr * Expr
    | Sub of Expr * Expr
    | Mult of Expr * Expr
    | Div of Expr * Expr
    | Pow of Expr * Expr

let rec stringify expr =
    match expr with
    | Con(x) -> string x
    | Var(x) -> string x
    | Add(x, y) -> sprintf "(%s + %s)" (stringify x) (stringify y)
    | Sub(x, y) -> sprintf "(%s - %s)" (stringify x) (stringify y)
    | Mult(x, y) -> sprintf "(%s * %s)" (stringify x) (stringify y)
    | Div(x, y) -> sprintf "(%s / %s)" (stringify x) (stringify y)
    | Pow(x, y) -> sprintf "(%s ** %s)" (stringify x) (stringify y)

type Expr with
    override this.ToString() = stringify this

Однако у него действительно есть неприятный побочный эффект

warning FS0060: Override implementations in augmentations are now deprecated. Override implementations should be given as part of the initial declaration of a type.
person Daniel Fabian    schedule 03.08.2013
comment
Да, это было то, с чего я действительно начал, но я думаю, что я думал, что это сообщение было ошибкой, а не предупреждением. Тем не менее, ваш метод может быть самым простым, даже если он устарел. Мне все еще любопытно, знает ли кто-нибудь еще здесь другой способ, такой же простой, но не устаревший. - person luksan; 04.08.2013
comment
Интересно, что это работает, только если я выполняю весь сценарий сразу (выделив его в VS и нажав ALT-ENTER). ЕСЛИ я пытаюсь выполнить определение типа и расширение по отдельности, я получаю error FS0854: Method overrides and interface implementations are not permitted here, потому что компилятор F # не может переопределить метод для уже существующего типа. Возможно, это двойное поведение является причиной того, что они не рекомендуют это поведение. - person luksan; 04.08.2013
comment
Что ж, при компиляции тип augmentation попадает в тот же класс. В то время как тип extension заканчивается как метод расширения. Однако синтаксис такой же, поэтому я как бы понимаю, что вы должны выполнять его одновременно. (Не обязательно говорить, это очень интуитивно понятно) - person Daniel Fabian; 05.08.2013
comment
Ах, я забыл об этой разнице. MSDN, похоже, избегает термина расширение и вместо этого использует внутреннее расширение по сравнению с необязательным расширением, чтобы различать их: msdn.microsoft.com/en-us/library/dd233211.aspx. Затем статья скользит вниз и говорит: неявное расширение. Он по-прежнему не говорит, что происходит, когда вы выполняете его в немедленном режиме (независимо от того, является ли он внутренним или необязательным); но я полагаю, что каждый блок кода, который вы выполняете, рассматривается как отдельный файл, так что я думаю, это сделает его необязательным. - person luksan; 05.08.2013
comment
действительно ли этот ответ работает для остальной части .net (C # и т. д.), как если бы он был определен непосредственно в типе? - person Maslow; 29.08.2016
comment
это идея, да - person Daniel Fabian; 31.08.2016

Как насчет решения, которое даже не требует расширения типа.

Вместо этого определите тип со статическим членом, который является строковым (нам нужен фиктивный тип, поскольку type a ... and b требует, чтобы b был типом

type Num = string //missing
type Name = string //missing
type Expr = 
    | Con of Num
    | Var of Name
    | Add of Expr * Expr
    | Sub of Expr * Expr
    | Mult of Expr * Expr
    | Div of Expr * Expr
    | Pow of Expr * Expr
    override this.ToString() = type_dummy.stringify this
and type_dummy = 
    static member stringify expr =
        let stringify = type_dummy.stringify
        match expr with
        | Con(x) -> string x
        | Var(x) -> string x
        | Add(x, y) -> sprintf "(%s + %s)" (stringify x) (stringify y)
        | Sub(x, y) -> sprintf "(%s - %s)" (stringify x) (stringify y)
        | Mult(x, y) -> sprintf "(%s * %s)" (stringify x) (stringify y)
        | Div(x, y) -> sprintf "(%s / %s)" (stringify x) (stringify y)
        | Pow(x, y) -> sprintf "(%s ** %s)" (stringify x) (stringify y)
person John Palmer    schedule 03.08.2013
comment
Да, но для этого необходимо, чтобы два типа были смежными в исходном файле, чего я как бы пытался избежать. Тем не менее, это может быть лучше, чем наносить его прямо на шрифт. - person luksan; 04.08.2013