Case statement that not working as expected

G-d willing

Hello,
I have an issue with the following case that is weird to me.
Here is the function with the problem:

convertTypeByProduct : Optional SomeType -> ProductType -> Date -> Optional SomeOtherType
convertTypeByProduct someType productType today =
  case productType of
      TypeOne -> mustHaveValue
      TypeTwo -> mustHaveValue
      TypeThree -> None
    where
      mustHaveValue =
        let valueType = doSomething someType in
        if isNone valueType
        then error $ "No Value Error | Product Type = " <> (show productType)
        else Some SomeOtherType with
                intValue = 1

It is not important what is SomeType nor SomeOtherType and etc…
The problem I am having is when I call the convertTypeByProduct function with productType = TypeThree.
I am getting the error when I shouldn’t:

message = "No Value Error | Product Type = TypeThree"

I have no idea what the problem is here. What am I missing in here?

G-d willing

I just changed the code to the following, and it is working.

convertTypeByProduct : Optional SomeType -> ProductType -> Date -> Optional SomeOtherType
convertTypeByProduct someType productType today =
  case productType of
      TypeOne -> mustHaveValue 1
      TypeTwo -> mustHaveValue 2
      TypeThree -> trace "Reached TypeThree" None
    where
      mustHaveValue value =
        let valueType = doSomething someType in
        if isNone valueType
        then error $ "No Value Error | Product Type = " <> (show productType) <> " | value = " <> (show value)
        else Some SomeOtherType with
                intValue = 1

I am not sure why without adding the value it didn’t work, and why adding the value resolved the problem.

Daml is a strict language. The bindings in your where clause are always evaluated even if they might not be used. So in your first example, even if you pass TypeThree, mustHaveValue is evaluated and produces the error.

In your second example, mustHaveValue is still evaluated. However now mustHaveValue is a function so evaluating it produces a lambda instead of producing an error because it is not yet applied to an argument.

G-d willing

Thanks for the explanation.
But isn’t everything in DAML a function? So this behavior was still not expected to behave as it should…
Anyway, how do you think is the correct way to solve it without forcing an unwanted argument, in order to make the where clause work as expected?

No, only things of type a -> b for some type a and b are functions. Functional programming does not mean that everything is a function.

I think making it a function is your best option here. In some cases, you could switch to a let instead but you’re sharing the code here so that doesn’t work nicely. I’d change the argument though to capture the one the behavior of the function depends on:

convertTypeByProduct : Optional SomeType -> ProductType -> Date -> Optional SomeOtherType
convertTypeByProduct someType productType today =
  case productType of
      TypeOne -> mustHaveValue someType
      TypeTwo -> mustHaveValue someType
      TypeThree -> trace "Reached TypeThree" None
    where
      mustHaveValue someType =
        let valueType = doSomething someType in
        if isNone valueType
        then error $ "No Value Error | Product Type = " <> (show productType) <> " | value = " <> (show value)
        else Some SomeOtherType with
                intValue = 1
1 Like

Hi @cohen.avraham,

Not everything in Daml is a function: we also have values (1, "hello", None, etc.).

What @cocreature meant is that your code is essentially rewritten to (first snippet):

convertTypeByProduct : Optional SomeType -> ProductType -> Date -> Optional SomeOtherType
convertTypeByProduct someType productType today =
  let mustHaveValue =
        let valueType = doSomething someType in
        if isNone valueType
        then error $ "No Value Error | Product Type = " <> (show productType)
        else Some SomeOtherType with
                intValue = 1
  in
  case productType of
      TypeOne -> mustHaveValue
      TypeTwo -> mustHaveValue
      TypeThree -> None

which may make the problem more apparent: when executing that function, the first thing that the Daml interpreter does is evaluating the value of the right hand side of the mustHaveValue = ... assignment. If your argument happens to make doSomething return None, this evaluation will trigger an error (due to the error function being called), before Daml even looks at the case productType of line.

In the second case, we have essentially the same scenario, except that it roughly gets rewritten to:

convertTypeByProduct : Optional SomeType -> ProductType -> Date -> Optional SomeOtherType
convertTypeByProduct someType productType today =
  let mustHaveValue = \value ->
        let valueType = doSomething someType in
        if isNone valueType
        then error $ "No Value Error | Product Type = " <> (show productType) <> " | value = " <> (show value)
        else Some SomeOtherType with
                intValue = 1
  in
  case productType of
      TypeOne -> mustHaveValue 1
      TypeTwo -> mustHaveValue 2
      TypeThree -> trace "Reached TypeThree" None

because the syntax f x = body is a shorthand for f = \x -> body. The Daml interpreter is still going to look at the mustHaveValue = ... assignment first, but in this case, because the right hand side is a function (specifically, an anonymous function, also known as a lambda), its evaluation is deferred until it is applied to an argument. Now, in this case, the mustHaveValue function will not be called if the productType argument matches the TypeThree constructor, and therefore, if TypeThree was the only value that made doSomething return None, you no longer have an error.

So hopefully that clarifies the problem a little bit.

Now, what to do about it? The fundamental issue here is that you are calling error, which is hard to use because it produces a side-effect. It is generally best to keep your Daml code as pure as possible: as you can see here, it is sometimes a bit difficult to reason about the order of evaluation of a Daml expression, and pure code does not care about order of evaluation (barring non-termination).

How to turn this code pure? One way to collect error conditions without calling error explicitly is to use a “success/error” wrapping type. In your case, you’re almost already doing that: doSomething returns an Optional, which in some contexts can be interpreted as success = Some and error = None. Further, convertTypeByProduct is already defined to return an Optional itself, so one option would be to simply carry on the Optional value and have None mean “there was an error”:

convertTypeByProduct : Optional SomeType -> ProductType -> Date -> Optional SomeOtherType
convertTypeByProduct someType productType today =
  let mustHaveValue = \value ->
        let valueType = doSomething someType in
        if isNone valueType
        then None
        else Some SomeOtherType with
                intValue = 1
  in
  case productType of
      TypeOne -> mustHaveValue 1
      TypeTwo -> mustHaveValue 2
      TypeThree -> trace "Reached TypeThree" None

This assumes the code that calls convertTypeByProduct considers None to be an error and handles it appropriately.

Note that if doSomething already returns None exclusively for TypeThree values, using some syntactic sugar, I believe this can be rewritten as

convertTypeByProduct : Optional SomeType -> ProductType -> Date -> Optional SomeOtherType
convertTypeByProduct someType productType today = do
  valueType <- doSomething someType
  return $ SomeOtherType 1

but that may be a topic for another time.

One thing we’ve lost with this approach is that we no longer have an error message, whereas we had one in the original approach. This is because Optional does not have any argument to its second (None) constructor. There’s another type we can use, though, which can have a similar interpretation: Either. Specifically, Either Text SomeType in this case, where you could have the calling code interpret Right SomeType as a success and Left Text as an error with the error message attached.

This would look something like:

convertTypeByProduct : Optional SomeType -> ProductType -> Date -> Either Text SomeOtherType
convertTypeByProduct someType productType today =
  let mustHaveValue =
        let valueType = doSomething someType in
        if isNone valueType
        then Left $ "No Value Error | Product Type = " <> (show productType)
        else Right SomeOtherType with
                intValue = 1
  in
  case productType of
      TypeOne -> mustHaveValue
      TypeTwo -> mustHaveValue
      TypeThree -> Left "given TypeThree"

Note the changed return type for convertTypeByProduct.

Finally, if you also rewrote doSomething itself to return an Either, you could do something like:

convertTypeByProduct : Optional SomeType -> ProductType -> Date -> Either Text SomeOtherType
convertTypeByProduct someType productType today =
  let mustHaveValue = do
        valueType <- doSomething someType
        return $ SomeOtherType 1
  in
  case productType of
      TypeOne -> mustHaveValue
      TypeTwo -> mustHaveValue
      TypeThree -> Left "given TypeThree"

assuming that doSomething returns a Left Text with the appropriate error message itself.

2 Likes

@Gary_Verhaegen, you always surprise me with your full detailed answers. I have no way of expressing my appreciation for that.
Regarding your answer, well, the error was placed on purpose since the business logic is asking me to fail that process in case valueType is None whenever the productType == TypeThree.