区分联盟-允许模式匹配但限制构造

时间:2019-01-17 15:41:46

标签: f# pattern-matching discriminated-union

我有一个F#区分联盟,我想在其中将一些“构造函数逻辑”应用于构造联盟案例中使用的任何值。假设联合看起来像这样:

type ValidValue =
| ValidInt of int
| ValidString of string
// other cases, etc.

现在,我想对实际传入的值应用一些逻辑,以确保它们有效。为了确保我最终不会处理实际上不是有效的ValidValue实例(尚未使用验证逻辑构造过的实例),我将构造函数设为私有并公开了一个强制执行构造它们的逻辑。

type ValidValue = 
    private
    | ValidInt of int
    | ValidString of string

module ValidValue =
    let createInt value =
        if value > 0 // Here's some validation logic
        then Ok <| ValidInt value
        else Error "Integer values must be positive"

    let createString value =
        if value |> String.length > 0 // More validation logic
        then Ok <| ValidString value
        else Error "String values must not be empty"

此方法有效,使我可以强制执行验证逻辑,并确保ValidValue的每个实例均有效。但是,问题在于,该模块之外的任何人都无法在ValidValue上进行模式匹配以检查结果,从而限制了“受歧视联盟”的用处。

我希望允许外部用户像其他任何DU一样继续进行模式匹配并使用ValidValue,但是如果它具有私有构造函数,那是不可能的。我能想到的唯一解决方案是使用私有构造函数将DU中的每个值包装为单例联合类型,并将实际的ValidValue构造函数公开。这会将案例暴露给外部,从而使它们能够与之匹配,但仍主要防止外部调用者构造它们,因为实例化每个案例所需的值将具有私有构造函数:

type VInt = private VInt of int
type VString = private VString of string

type ValidValue = 
| ValidInt of VInt
| ValidString of VString

module ValidValue =
    let createInt value =
        if value > 0 // Here's some validation logic
        then Ok <| ValidInt (VInt value)
        else Error "Integer values must be positive"

    let createString value =
        if value |> String.length > 0 // More validation logic
        then Ok <| ValidString (VString value)
        else Error "String values must not be empty"

现在,调用方可以匹配ValidValue的大小写,但是他们无法读取联合大小写内部的实际整数和字符串值,因为它们被包装在具有私有构造函数的类型中。可以使用每种类型的value函数来解决此问题:

module VInt =
    let value (VInt i) = i

module VString =
    let value (VString s) = s

不幸的是,现在增加了呼叫者的负担:

// Example Caller
let result = ValidValue.createInt 3

match result with
| Ok validValue ->
    match validValue with
    | ValidInt vi ->
        let i = vi |> VInt.value // Caller always needs this extra line
        printfn "Int: %d" i
    | ValidString vs ->
        let s = vs |> VString.value // Can't use the value directly
        printfn "String: %s" s
| Error error ->
    printfn "Invalid: %s" error

是否有更好的方法来强制执行我一开始想要的构造函数逻辑,而又不会增加其他地方的负担?

2 个答案:

答案 0 :(得分:7)

您可以有专用的构造函数,但是要公开具有相同名称的公共活动模式。这是定义和使用它们的方式(为简便起见,省略了创建函数):

import support_functions


def main_menu():
    print('Fechamento de Caixa')
    numero = input('Numero: ')
    dados = [numero]
    dados = dados.__add__(menu_a())
    dados = dados.__add__(menu_b())
    dados = dados.__add__(menu_c())
    print('(D) Cartões')
    print(' (D-a) POS')
    dados = dados.__add__(['(D) Cartões', '(D-a) POS'])
    dados = dados.__add__(menu_d_a_a())
    dados = dados.__add__(menu_d_a_b())
    dados = dados.__add__(menu_d_a_c())
    dados = dados.__add__(menu_d_a_d())
    dados = dados.__add__(menu_e())
    dados = dados.__add__(menu_f())
    dados = dados.__add__(menu_g())
    dados = dados.__add__(menu_h())
    dados = dados.__add__(menu_i())
    dados = dados.__add__(menu_j())
    dados = dados.__add__(menu_k())
    dados = dados.__add__(menu_l())
    dados = dados.__add__(menu_m())
    dados = dados.__add__(menu_n())
    return dados


def menu_a():
    a = support_functions.convert_and_replace(input('(A) Valor em Dinheiro: R$'))
    return ['(A) Valor em Dinheiro', a]


def menu_b():
    b = support_functions.convert_and_replace(input('(B) Valor total de Sangrias: R$'))
    return ['(B) Valor total de Sangrias', b]


def menu_c():
    print('(C) Cheques')
    c_a = support_functions.convert_and_replace(input('  (C-a) Valor de Cheques à Vista: R$'))
    c_b = support_functions.convert_and_replace(input('  (C-b) Valor de Cheques à Prazo: R$'))
    c = c_a + c_b
    return ['(C) Cheques', '(C-a) Valor de Cheques à Vista', c_a, '(C-b) Valor de Cheques à Prazo', c_b,
            '# Total (C)', c]


def menu_d_a_a():
    print('     (D-a-a) Crédito')
    visa_cr = support_functions.convert_and_replace(input('           Visa: R$'))
    mastercard_cr = support_functions.convert_and_replace(input('         Mastercard: R$'))
    elo_cr = support_functions.convert_and_replace(input('            Elo: R$'))
    banricompras_30 = support_functions.convert_and_replace(input('           Banricompras 30 Dias: R$'))
    banricompras_45 = support_functions.convert_and_replace(input('           Banricompras 45 Dias: R$'))
    banricompras_60 = support_functions.convert_and_replace(input('           Banricompras 60 Dias: R$'))
    banricompras_90 = support_functions.convert_and_replace(input('           Banricompras 90 Dias: R$'))
    amex = support_functions.convert_and_replace(input('          American Express: R$'))
    cabal_cr = support_functions.convert_and_replace(input('          Cabal: R$'))
    # diners_cr = support_functions.convert_and_replace(input('         Diners: R$'))
    hiper_cr = support_functions.convert_and_replace(input('          Hipercard: R$'))
    verdecard_cr = support_functions.convert_and_replace(input('          Verdecard: R$'))
    #  Totalizador Credito
    d_a_a = visa_cr + mastercard_cr + elo_cr + banricompras_30 + banricompras_45 + banricompras_60 + banricompras_90 \
        + cabal_cr + amex + hiper_cr + verdecard_cr
    return ['(D-a-a) Crédito', 'Visa', visa_cr, 'Mastercard', mastercard_cr, 'Elo', elo_cr, 'Banricompras 30 Dias',
            banricompras_30, 'Banricompras 45 Dias', banricompras_45, 'Banricompras 60 Dias', banricompras_60,
            'Banricompras 90 Dias', banricompras_90, 'American Express', amex, 'Cabal', cabal_cr,  'Hipercard',
            hiper_cr, 'Verdecard', verdecard_cr, '# Total (D-a-a)', d_a_a]


def menu_d_a_b():
    print('     (D-a-b) Débito')
    banricompras_db = support_functions.convert_and_replace(input('           Banricompras: R$'))
    cabal_db = support_functions.convert_and_replace(input('          Cabal: R$'))
    elo_db = support_functions.convert_and_replace(input('            Elo: R$'))
    mastercard_db = support_functions.convert_and_replace(input('         Mastercard: R$'))
    visa_db = support_functions.convert_and_replace(input('           Visa: R$'))
    # Totalizador Debito
    d_a_b = banricompras_db + cabal_db + elo_db + mastercard_db + visa_db
    return ['(D-a-b) Débito', 'Banricompras', banricompras_db, 'Cabal', cabal_db, 'Elo', elo_db, 'Mastercard',
            mastercard_db, 'Visa', visa_db, '# Total (D-a-b)', d_a_b]


def menu_d_a_c():
    print('     (D-a-c) Parcelados')
    elo_pc = support_functions.convert_and_replace(input('            Elo: R$'))
    hiper_pc = support_functions.convert_and_replace(input('          Hipercard: R$'))
    mastercard_pc = support_functions.convert_and_replace(input('         Mastercard: R$'))
    verdecard_pc = support_functions.convert_and_replace(input('          Verdecard: R$'))
    visa_pc = support_functions.convert_and_replace(input('           Visa: R$'))
    # Totalizador Parcelado
    d_a_c = elo_pc + hiper_pc + mastercard_pc + verdecard_pc + visa_pc
    return ['(D-a-c) Parcelados', 'Elo', elo_pc, 'Hipercard', hiper_pc, 'Mastercard', mastercard_pc, 'Verdecard',
            verdecard_pc, 'Visa', visa_pc, '# Total (D-a-c)', d_a_c]


def menu_d_a_d():
    print('     (D-a-d) Vale/Beneficio')
    alelo_al = support_functions.convert_and_replace(input('          Alelo Alimentação: R$'))
    alelo_rf = support_functions.convert_and_replace(input('          Alelo Refeição: R$'))
    banricard_al = support_functions.convert_and_replace(input('          Banricard Alimentação: R$'))
    banricard_cb = support_functions.convert_and_replace(input('          Banricard Combustivel: R$'))
    sodexo_al = support_functions.convert_and_replace(input('         Sodexo Alimentação: R$'))
    sodexo_cb = support_functions.convert_and_replace(input('         Sodexo Combustivel: R$'))
    vr_al = support_functions.convert_and_replace(input('         VR Alimentação: R$'))
    vr_bn = support_functions.convert_and_replace(input('         VR Beneficio: R$'))
    vr_cb = support_functions.convert_and_replace(input('         VR Combustivel: R$'))
    vr_rf = support_functions.convert_and_replace(input('         VR Refeição: R$'))
    ticket_al = support_functions.convert_and_replace(input('         Ticket Alimentação: R$'))
    ticket_rt = support_functions.convert_and_replace(input('         Ticket Restaurante: R$'))
    shell_rf = support_functions.convert_and_replace(input('          Shell Resgate Fácil: R$'))
    # Totalizador Vale/Beneficio
    d_a_d = alelo_al + alelo_rf + banricard_al + banricard_cb + sodexo_al + sodexo_cb + vr_al + vr_bn + vr_cb + vr_rf + ticket_al + ticket_rt + shell_rf
    return ['(D-a-d) Vale/Beneficio', 'Alelo Alimentação', alelo_al, 'Alelo Refeição', alelo_rf,
            'Banricard Alimentação', banricard_al, 'Banricard Combustivel', banricard_cb, 'Sodexo Alimentação',
            sodexo_al, 'Sodexo Combustivel', sodexo_cb, 'VR Alimentação', vr_al, 'VR Beneficio', vr_bn,
            'VR Combustivel', vr_cb, 'VR Refeição', vr_rf, 'Ticket Alimentação', ticket_al, 'Ticket Restaurante',
            ticket_rt, 'Shell Resgate Fácil', shell_rf, '# Total (D_a_d', d_a_d]


def menu_e():
    e = support_functions.convert_and_replace(input('(E) Notas à Prazo: R$'))
    return ['(E) Notas à Prazo', e]


def menu_f():
    f = support_functions.convert_and_replace(input('(F) Recarga: R$'))
    return ['(F) Recarga', f]


def menu_g():
    g = support_functions.convert_and_replace(input('(G) Quitação de Notas à Prazo: R$'))
    return ['(G) Quitação de Notas à Prazo', g]


def menu_h():
    h = support_functions.convert_and_replace(input('(H) Pagamento de Notas Fiscais: R$'))
    return ['(H) Pagamento de Notas Fiscais', h]


def menu_i():
    i: int = input('(I) Leitura Café: ')
    return ['(I) Leitura Café', i]


def menu_j():
    j: int = input('(J) Lanches Entrada: ')
    return ['(J) Lanches Entrada', j]


def menu_k():
    k: int = input('(K) Lanches Saida: ')
    return ['(K) Lanches Saida', k]


def menu_l():
    l = support_functions.convert_and_replace(input('(L) Vales Troco Recebidos: R$'))
    return ['(L) Vales Troco Recebidos', l]


def menu_m():
    m = support_functions.convert_and_replace(input('(M) Vales Troco Emitidos: R$'))
    return ['(M) Vales Troco Emitidos', m]


def menu_n():
    n = support_functions.convert_and_replace(input('(N) Troco Final: R$'))
    return ['(N) Troco Final', n]

答案 1 :(得分:2)

除非有特殊的原因要求使用区分式联合,否则鉴于您提供的特定用例,这听起来好像根本就不需要区分式联合,因为活动模式会更有用。例如:

let (|ValidInt|ValidString|Invalid|) (value:obj) = 
    match value with
    | :? int as x -> if x > 0 then ValidInt x else Invalid
    | :? string as x -> if x.Length > 0 then ValidString x else Invalid
    | _ -> Invalid

此时,呼叫者可以匹配并确保已应用该逻辑。

match someValue with
| ValidInt x -> // ...
| _ -> // ...