Alexander Zeitler
Alexander Zeitler

Reputation: 13089

Return Union Type instead of Union Case

I have a union type like this

  type AccountCreated =
    { Owner: string
      AccountId: Guid
      CreatedAt: DateTimeOffset
      StartingBalance: decimal }

  type AccountDebited =
    { To: Guid
      From: Guid
      Description: string
      Time: DateTimeOffset
      Amount: decimal }

  type AccountCredited =
    { To: Guid
      From: Guid
      Description: string
      Time: DateTimeOffset
      Amount: decimal }

  type AccountEvent =
    | Created of AccountCreated
    | AccountCredited of AccountCredited
    | AccountDebited of AccountDebited

And another Union type like this:

 type RegisteredAccount = {
      Owner: string
      Balance: decimal
      AccountId: Guid }
  
  type Account =
    | Unregistered
    | Registered of RegisteredAccount

There's a function evolve:

  let evolve state event: Account =
    match event with
      | Created accountCreated ->
          { AccountId = accountCreated.AccountId
            Owner = accountCreated.Owner
            Balance = accountCreated.StartingBalance }
      | AccountDebited accountDebited ->
          match state with
          | Registered s -> 
              { s with
                  Balance = s.Balance - accountDebited.Amount }
          | _ -> failwith "unregistered account can't be debited"
      | _ -> failwith "todo: other cases"

evolve should be used using List.fold:

let build = List.fold evolve
let rebuild = build Unregistered

If I don't explicitely specify the return type of evolve as Account, I get this error for the line let build = List.fold evolve:

Type mismatch. Expecting a 'Account -> AccountEvent -> Account' but given a 'Account -> AccountEvent -> RegisteredAccount' The type 'Account' does not match the type 'RegisteredAccount'

If set the return type of evolve, I get the compiler error in the pattern match for Created and one similar for Registered inside the AccountDebited pattern match.

This expression was expected to have type 'Account' but here has type 'RegisteredAccount'

How can I solve this?

Upvotes: 0

Views: 75

Answers (1)

Tomas Petricek
Tomas Petricek

Reputation: 243051

I think the issue in your code is that the evolve function tries to return a value of the RegisteredAccount record type rather than a value of the Account union type.

I see you want to use Unregistered value (of type Account) as the initial value and you also added a type annotation specifying that the return type of evolve should be Account. I think this is actually what you want. The only thing missing is that you need to wrap the returned values using the Registered union case to turn RegisteredAccount into Account.

The following type-checks fine for me:

let evolve state event: Account =
  match event with
    | Created accountCreated ->
        { AccountId = accountCreated.AccountId
          Owner = accountCreated.Owner
          Balance = accountCreated.StartingBalance } |> Registered
    | AccountDebited accountDebited ->
        match state with
        | Registered s -> 
            { s with
                Balance = s.Balance - accountDebited.Amount } |> Registered
        | _ -> failwith "unregistered account can't be debited"
    | _ -> failwith "todo: other cases"

All I had to do is to add |> Registered in two places where you are returning a record!

Upvotes: 3

Related Questions