F# 是否可以识别 DU 的重叠并使用正确的重叠本身?

发布于 2025-01-09 19:49:53 字数 3867 浏览 1 评论 0原文

type GenericResult =
    | Ok
    | Error of string

type LoginResult =
    | Ok
    | UserNotFound
    | WrongPassword

let check something:GenericResult =
    match something with
    //| true -> Ok // error:This expression was expected to be of type "GenericREsult" but here has type "LoginResult"
    | true -> GenericResult.Ok // I'm forced to specify GenericResult.Ok
    | false -> Error "aargg!"

let checkLogin something:LoginResult =
    match something with
    | true -> Ok // here I don't need to specify the DU because this is defined after
    | _ -> WrongPassword

我想在这两种方法中只使用“Ok”,而不需要指定 DU。
我发现,如果值发生冲突,最后一个是“预定义的”。

理想情况下,我希望拥有某种继承权
在另一个 DU 中重用一个 DU 的一部分。
例如:

type GenericResult =
    | Ok
    | Error of string

type LoginResult =
    //| GenericResult.Ok
    | UserNotFound
    | WrongPassword

type SaveResult =
    | Created
    | Updated
    //| GenericResult.Error


let checkLogin something: LoginResult | GenericResult.Ok =
    match something with
    | true -> Ok 
    | _ -> WrongPassword

[编辑] 我觉得需要此功能的真实场景是来自 3 个不同逻辑类的 3 个不同结果。
将来会出现更多情况,因此重复 DU 值的倍数将会增加。

// DUs ordered from the most specific to the most generic

type BalanceUpdateResult = 
| Created
| Updated
| InvalidRequest of string

type DeleteResult =
| Ok
| InvalidRequest of string

type Result<'T> =
| Ok of 'T
| NotValid of string
| Error of string

目标是在使用者中拥有干净的匹配语法,例如,DU 的值最终将用于引发异常或返回创建的值。

// balance update function (result is BalanceUpdateResult):
    match result with
    | Created -> this.createOkWithStatus 201 
    | Updated -> this.createOkWithStatus 200
    | InvalidRequest error -> this.createErrorForConflict error

// company creation function (result is Result<Company>):
    match result with 
    | Result.Ok newItem -> 
        context.Logger.Log $"Company created. New Id:{newItem.Id}, Name:{newItem.Name}."
        this.createCreated newItem
    | NotValid error -> base.createErrorForConflict error
    | Error error -> base.createError error

例如,在第二种情况下,InvalidRequest 不被接受,因为它属于错误的 DU。
必须在各处指定 DU 会导致混乱,如下例所示(请参阅许多结果<_>。):

    interface ICompanyLogic with
        member this.Create(company:Company):Result<Company> =
            match normalize company |> validate with
            | NotValid msg -> Result<_>.NotValid msg
            | Valid validCompany ->
                match companyRepository.Exists(validCompany.Name) with 
                | true -> Result<_>.NotValid($"A company with name \"{validCompany.Name}\" already exists.")
                | _ -> 
                    let newCompany = assignNewId validCompany
                    companyRepository.Create(newCompany)
                    Result<_>.Ok(newCompany)
            
        member this.Update (company:Company):Result<Company> =
            let checkNameExists company = 
                match companyRepository.GetByName company.Name with
                | Some c when c.Id <> company.Id -> NotValid $"A company with name \"{company.Name}\" already exists."
                | _ -> Valid company
         
            match normalize company |> validate with
            | NotValid msg -> Result<_>.NotValid msg
            | Valid c -> match checkNameExists c with
                         | Valid c -> companyRepository.Update c; Result<_>.Ok c
                         | NotValid msg -> Result<_>.NotValid msg
            
type GenericResult =
    | Ok
    | Error of string

type LoginResult =
    | Ok
    | UserNotFound
    | WrongPassword

let check something:GenericResult =
    match something with
    //| true -> Ok // error:This expression was expected to be of type "GenericREsult" but here has type "LoginResult"
    | true -> GenericResult.Ok // I'm forced to specify GenericResult.Ok
    | false -> Error "aargg!"

let checkLogin something:LoginResult =
    match something with
    | true -> Ok // here I don't need to specify the DU because this is defined after
    | _ -> WrongPassword

I'd like to use just "Ok" in both the methods, without the need to specify the DU.
I see that in case of clashing of the value the last one is the "predefined".

Ideally I'd like to have a sort of inheritance
to reuse part of a DU in another DU.
For example:

type GenericResult =
    | Ok
    | Error of string

type LoginResult =
    //| GenericResult.Ok
    | UserNotFound
    | WrongPassword

type SaveResult =
    | Created
    | Updated
    //| GenericResult.Error


let checkLogin something: LoginResult | GenericResult.Ok =
    match something with
    | true -> Ok 
    | _ -> WrongPassword

[EDIT]
The real scenario where I feel the need for this feature is this with 3 different results from 3 different logic classes.
There will be in the future more cases so the multiplication of duplicated DU values will increase.

// DUs ordered from the most specific to the most generic

type BalanceUpdateResult = 
| Created
| Updated
| InvalidRequest of string

type DeleteResult =
| Ok
| InvalidRequest of string

type Result<'T> =
| Ok of 'T
| NotValid of string
| Error of string

The goal is to have a clean match syntax in the consumer, where the value of the DU will evenctually be used to raise an exception or to return the created value, for example.

// balance update function (result is BalanceUpdateResult):
    match result with
    | Created -> this.createOkWithStatus 201 
    | Updated -> this.createOkWithStatus 200
    | InvalidRequest error -> this.createErrorForConflict error

// company creation function (result is Result<Company>):
    match result with 
    | Result.Ok newItem -> 
        context.Logger.Log 
quot;Company created. New Id:{newItem.Id}, Name:{newItem.Name}."
        this.createCreated newItem
    | NotValid error -> base.createErrorForConflict error
    | Error error -> base.createError error

Here, for example, InvalidRequest is not accepted in the second case because it belongs to the wrong DU.
Having to specify the DU everywhere results in a mess like the following example (see the many Result<_>.):

    interface ICompanyLogic with
        member this.Create(company:Company):Result<Company> =
            match normalize company |> validate with
            | NotValid msg -> Result<_>.NotValid msg
            | Valid validCompany ->
                match companyRepository.Exists(validCompany.Name) with 
                | true -> Result<_>.NotValid(
quot;A company with name \"{validCompany.Name}\" already exists.")
                | _ -> 
                    let newCompany = assignNewId validCompany
                    companyRepository.Create(newCompany)
                    Result<_>.Ok(newCompany)
            
        member this.Update (company:Company):Result<Company> =
            let checkNameExists company = 
                match companyRepository.GetByName company.Name with
                | Some c when c.Id <> company.Id -> NotValid 
quot;A company with name \"{company.Name}\" already exists."
                | _ -> Valid company
         
            match normalize company |> validate with
            | NotValid msg -> Result<_>.NotValid msg
            | Valid c -> match checkNameExists c with
                         | Valid c -> companyRepository.Update c; Result<_>.Ok c
                         | NotValid msg -> Result<_>.NotValid msg
            

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

只涨不跌 2025-01-16 19:49:53

我认为实现您想要做的事情的最佳方法是从通用 Result 类型开始,该类型具有表示错误类型的类型参数:

type Result<'TError> =
    | Ok
    | Error of 'TError

这允许您使用不同的类型来表示错误,包括 string,还有另一个 DU 来捕获更具体的错误类型。然后,您可以将 GenericResultLoginResult 定义为两个类型别名:

type LoginError =
    | UserNotFound
    | WrongPassword

type GenericResult = Result<string>
type LoginResult = Result<LoginError>

要报告登录错误,您现在可以使用 Error WrongPassword 来包装特定错误在通用 Error 构造函数中。这两个函数的实现如下所示:

let check something:GenericResult =
  match something with
  | true -> Ok
  | false -> Error "aargg!"

let checkLogin something:LoginResult =
  match something with
  | true -> Ok
  | _ -> Error WrongPassword

I think the best way to achieve what you are trying to do would be to start with a generic Result type that has a type parameter representing the error type:

type Result<'TError> =
    | Ok
    | Error of 'TError

This allows you to use different types for representing errors, including string, but also another DU to capture more specific error types. You can then define GenericResult and LoginResult as two type aliases:

type LoginError =
    | UserNotFound
    | WrongPassword

type GenericResult = Result<string>
type LoginResult = Result<LoginError>

To report a login error, you would now use Error WrongPassword to wrap the specific error in the generic Error constructor. The implementation of your two functions looks as follows:

let check something:GenericResult =
  match something with
  | true -> Ok
  | false -> Error "aargg!"

let checkLogin something:LoginResult =
  match something with
  | true -> Ok
  | _ -> Error WrongPassword
金橙橙 2025-01-16 19:49:53

与 TypeScript 联合类型不同,F# DU 旨在组合且不可扩展 - 请参阅 Thomas 答案 了解使用此方法的解决方案。

由于 F# 不提供直接的解决方案,您可以考虑重命名诸如 InvalidRequest 之类的情况,以便更具体并帮助在阅读代码时区分它们。使用这些特定名称,您还可以将所有结果类型合并到一个大的 Event DU 中,就像在事件源系统中通常所做的那样:

type Event = 
// BalanceUpdateResult
| BalanceCreated
| BalanceUpdated
| BalanceUpdateError of string

// DeleteResult
| DeleteOk
| DeleteError of string

// ...

Unlike TypeScript union type, F# DU are meant to be composed and not extensible - see Thomas answer for a solution using this approach.

Since F# does not offer a direct solution, you may consider renaming cases like InvalidRequest in order to be more specific and to help differentiate them when reading the code. With these specific names, you can also merge all result types into a big Event DU like what's usually done in an event sourced system:

type Event = 
// BalanceUpdateResult
| BalanceCreated
| BalanceUpdated
| BalanceUpdateError of string

// DeleteResult
| DeleteOk
| DeleteError of string

// ...
雨后彩虹 2025-01-16 19:49:53

好的,正如 Romain 所解释的,多个 DU 无法解决我的问题。
我决定使用内置的 type Result< 'T,'TError>
它使我能够避免创建许多不可避免地会发生名称冲突的 DU,从而强制在代码中使用完整的 DU 前缀。
我通过 Thomas 回复中鼓舞人心的示例解决了促使我创建自定义 DU 的问题。
(结果<,>)我有可能出现明显的错误或正常。
(注意 ResultResult

type ICompanyLogic =
    abstract member Create:Company -> Result<Company, string> // CreateResult
    abstract member Update:Company -> Result<Company, string> // UpdateResult
    abstract member Delete:string -> Result<unit,string>  // DeleteResult
    
type BalanceUpdateResult = 
| Created
| Updated

type IBalanceLogic =
    abstract member CreateOrUpdate: request:BalanceUpdateRequest -> Result<BalanceUpdateResult, string>

除了 BalanceUpdateResult 之外,所有其他替换的 DU 都购买 <代码>结果<'T,'TError>。
我只是为特定任务维护了几个:

type CompanyValidation = Valid of Company | NotValid of string
type ValidateResult = Valid | NotValid of string

最后使用此解决方案:

  • 我不需要定义许多 DU,
  • 我可以自定义结果...在我想要的尽可能多的值内(将子 DU 存储在 Ok 或错误联合案例)
  • 我不需要使用前缀或使用同义词来避免冲突(代码结果更干净)

Ok, as explained by Romain multiple DUs cannot solve my problem.
I decided to use the built-in type Result<'T,'TError>.
It allows me to avoid create many DUs that inevitably will have clash of names, forcing the use the full DU prefix in the code.
I solved the problem that drove me to create custom DUs with the inspiring example from Thomas reply.
(with Result<,>) I have the possibility to have dinstinct Errors or Oks.
(note the Result<unit,_> and the Result<BalanceUpdateRequest,_>)

type ICompanyLogic =
    abstract member Create:Company -> Result<Company, string> // CreateResult
    abstract member Update:Company -> Result<Company, string> // UpdateResult
    abstract member Delete:string -> Result<unit,string>  // DeleteResult
    
type BalanceUpdateResult = 
| Created
| Updated

type IBalanceLogic =
    abstract member CreateOrUpdate: request:BalanceUpdateRequest -> Result<BalanceUpdateResult, string>

Apart BalanceUpdateResult all the other DUs where replaced buy the Result<'T,'TError>.
I just maintained a couple one for specific tasks:

type CompanyValidation = Valid of Company | NotValid of string
type ValidateResult = Valid | NotValid of string

In the end with this solution:

  • I don't need to define many DUs
  • I can customize the Result... within as many values I want (storing a sub-DU in the Ok or Error union case)
  • I don't need to use prefix or use synonims to avoid clash (code result much cleaner)
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文