Is it possible to have F# able to recognize an overlap of DU and use the right one itself?

Viewed 82
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 $"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($"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
            
3 Answers

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

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

// ...

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)
Related