【问题标题】:Discriminated Union - Allow Pattern Matching but Restrict Construction可区分联合 - 允许模式匹配但限制构造
【发布时间】:2019-06-11 20:35:43
【问题描述】:

我有一个 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

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

【问题讨论】:

    标签: f# pattern-matching discriminated-union


    【解决方案1】:

    你可以有私有的 case 构造函数,但可以公开同名的公共活动模式。以下是您将如何定义和使用它们(为简洁起见省略了创建函数):

    module Helpers =
        type ValidValue = 
            private
            | ValidInt of int
            | ValidString of string
    
        let (|ValidInt|ValidString|) = function
            | ValidValue.ValidInt i -> ValidInt i
            | ValidValue.ValidString s -> ValidString s
    
    module Usage =
        open Helpers
    
        let validValueToString = function
            | ValidInt i -> string i
            | ValidString s -> s
        // ? Easy to use ✔
    
        // Let's try to make our own ValidInt ?
        ValidInt -1
        // error FS1093: The union cases or fields of the type
        // 'ValidValue' are not accessible from this code location
        // ? Blocked by the compiler ✔
    

    【讨论】:

    • 我看到了这样的 Haskell 答案,使用视图模式,它看起来确实是一个可能的中间立场。我唯一不喜欢的是 Active Patterns 不会对案例强制进行详尽的匹配,因此如果添加了新的联合案例或开发人员不知道所有可能的活动模式。尽管如此,考虑到语言的限制,这可能是我们能做的最好的事情。
    • @AaronM.Eshbach 实际上,这是一个完整的活动模式,因此您可以进行详尽的检查。这是你没有的部分活动模式。 See here
    • 不幸的是,当我实际尝试实施此解决方案时,我收到了错误Active Patterns cannot return more than 7 possibilities。我相信这是因为我的 DU 有 16 个案例。关于如何解决这个问题的任何想法?
    • @AaronM.Eshbach 我从来没有听说过这个限制!这似乎很随意 ? 不幸的是,除了将您的类型拆分为具有 7 个或更少子类型的较小类型之外,我不知道解决方法。回到你原来的解决方法,你可以将Value成员属性添加到VInt等,这样你就可以写vi.Value而不是vi |&gt; VInt.value
    【解决方案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 -> // ...
    | _ -> // ...
    

    【讨论】:

    • 我认为这并不能提供所有相同的好处。有一个受约束的类型会很好,这样任何接收实例的函数都可以获得数据某些属性的类型级别保证,并且不必进行任何进一步的检查/分支。
    • @TheQuickBrownFox 我同意你的观点,但是在许多情况下,这种类型的准依赖类型行为往往被用来代替验证。在后一种情况下,编译器无法保证,因为您的输入将来自外部,例如表单或数据库查询,因此您将出现运行时错误......
    • @s952163 由于私有构造函数,即使数据库查询也无法在不使用受约束类型模块中的代码的情况下将数据放入受约束类型。只要您确信该模块中的验证是正确的,那么您就可以在其他任何地方进行假设。当然这只是一个软保证,因为它依赖于正确实现的功能。
    • 我的例子是一个简化的用例。在我的真实场景中,我绝对想要一个有区别的联合,因为我代表一组固定的状态,每个状态围绕它们包含的值都有不同的规则。
    • @TheQuickBrownFox 绝对。我指的是编译时警告/错误确保您的代码正常工作的差异,以及当您进入非纯世界时所需的验证/错误/异常处理。
    猜你喜欢
    • 1970-01-01
    • 2018-02-03
    • 2014-05-07
    • 1970-01-01
    • 2019-01-10
    • 1970-01-01
    • 2019-08-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多