How can a function return dynamic data type

G-d willing

Hello,
Is there a way to write a function that returns a different data type according to it’s input?
For example, I want to achieve something like this - I know it does not compile, It is just to emphasize what I would like to achieve.

data DataType1 = DataType1 with
  someInt : Int

data DataType2 = DataType1 with
  someText : Text

data DataType3 = DataType1 with
  someDecimal : Decimal

funcTest : Int -> ????
funcTest value = case value of
    | 1 -> return DataType1 with someInt = 5
    | 2 -> return DataType2 with someText = "hello"
    | Otherwise -> return DataTy[e3 with someDecimal = 20.3

Currently, the way I can solve it is by creating a new data type that will store all of the 3 data types like the following:

  data AllDataTypes = AllDataTypes with
    dataType1: Optional DataType1
    dataType2: Optional DataType2
    dataType3: Optional DataType3

And according to the inout argument I will fill the correct data type, something like this:

funcTest : Int -> AllDataTypes
funcTest value = case value of
    | 1 -> return AllDataTypes with 
                               dataType1 = Some DataType1 with someInt = 5
                               dataType2 = None
                               dataType3 = None
    | 2 -> return AllDataTypes with 
                               dataType2 = Some DataType2 with someText = Hello
                               dataType1 = None
                               dataType3 = None
    |Otherwise return AllDataTypes with 
                               dataType3 = Some DataType3 with someDecimal = 20.3
                               dataType1 = None
                               dataType2 = None

Thanks

There is no way to choose a different type according to a different argument value. However, you can select an arbitrary type depending on the argument type.

To illustrate, let’s break down how you’re using Int in funcTest into a data type that represents only your possible arguments.

data N = One | Two | Other

And you would have a One, Two, and Other case in funcTest. However, this isn’t good enough to satisfy the rule I mentioned above.

However, consider another way of looking at the argument set to funcTest.

data One = One
data Two = Two
data Other = Other

Now I have types for the different arguments rather than just values. These I can relate to the desired return types as follows.

-- the first n -> z means "z depends on n";
--     it is solely about type variables
-- the second n -> z is the type of funcTest
class FuncTest n z | n -> z where
  funcTest : n -> z

-- I've skipped your DataType1 et al because I can just use
-- whatever result types I want

instance FuncTest One Int where
  funcTest One = 5

instance FuncTest Two Text where
  funcTest Two = "hello"

instance FuncTest Other Decimal where
  funcTest Other = 20.3

testFuncTest = script do
  -- these equality tests all pass and are well-typed
  funcTest One === 5
  funcTest Two === "hello"
  funcTest Other === 20.3
  {- does not compile, due to type error
  error:
    • Couldn't match type ‘Int’ with ‘Text’
        arising from a functional dependency between:
          constraint ‘FuncTest One Text’ arising from a use of ‘funcTest’
          instance ‘FuncTest One Int’ -}
  -- funcTest One === "hello"

This technique is an important part of the core operations of Daml. For example, exercise selects a return type depending on the contract type and choice type. It is not restricted to this order of determination, either: a result type can determine an argument type instead of the other way around. Or they can both determine each other.

I emphasize that an instance is selected at compile time. However, unlike with overloads, you can write your functions in such a way that “pass along” the FuncTest constraint and are thus polymorphic at compile-time themselves. createAndExercise is such a function. You might be interested in this forum post for more on class and instance:

2 Likes

Thank you @Stephen for a well explained answer. Allow me to make the question a bit more complicated (since this is the true situation I am having).

Consider DataType1, DataType2 & DataType3 having more than 1 property. All of the 3 data types have lots of same properties (names and types) but also, each one of them is having several unique properties.
Currently , I have a specific function that sets all the properties for each data type, and since there is a lot in common for all of the data types I would like to have one function that will take care of it. Something like this:

  data DataType1 = DataType1 with
    firstName: Text
    lastName: Text
    age: Int
    specificTypeValue1: Int

  data DataType2 = DataType2 with
    firstName: Text
    lastName: Text
    age: Int
    specificTypeValue2: Text

  data DataType3 = DataType3 with
    firstName: Text
    lastName: Text
    age: Int
    specificTypeValue3: Decimal

data DataTypeOption = One | Two | Three

I would like to achieve a function that will look something like the following:

testFunc : DataTypeOption -> ????
testFunc dataTypeOption = 
  let 
    firstName = "Hello"
    secondName = "World"
    age = 16
  case dataTypeOption of
    | One -> DataType1 with specificTypeValue1=5, ..
    | Two -> DataType2 with specificTypeValue2="another test", ..
    | Three -> DataType3 with specificTypeValue3=20.0, ..

This way I am having much less code, and also I am not duplicating my code.

In the dynamic case (I emphasize again that you cannot determine types based on values), you can take advantage of the fact that (a×b×c×d) + (a×b×c×e) + (a×b×c×f) = a×b×c×(d + e + f).

data DataTypeN = DataTypeN with
    firstName: Text
    lastName: Text
    age: Int
    specificTypeValue: STV

data STV = STV1 Int | STV2 Text | STV3 Decimal

data DataTypeOption = One | Two | Three

testFunc : DataTypeOption -> DataTypeN 
testFunc dataTypeOption = 
  DataTypeN with
    firstName = "Hello"
    lastName = "World"
    age = 16
    specificTypeValue = case dataTypeOption of
      One -> STV1 5
      Two -> STV2 "another test"
      Three -> STV3 20.0

Any set of fields in a record can always be refactored into a separate record type. See this forum post for many examples.

Meanwhile, in the type-to-type case I demonstrated in the previous example, you can perform the same factoring by using a type variable for specificTypeValue instead of the concrete STV sum type.

data DataTypeN stv = DataTypeN with
    firstName: Text
    lastName: Text
    age: Int
    specificTypeValue: stv

class FindSTV dto stv | dto -> stv where
  specificTypeValueOf : dto -> stv

data STV = STV1 Int | STV2 Text | STV3 Decimal

data One = One
data Two = Two
data Three = Three

testFunc : FindSTV dto stv => dto -> DataTypeN stv
testFunc dataTypeOption = 
  DataTypeN with
    firstName = "Hello"
    lastName = "World"
    age = 16
    specificTypeValue = specificTypeValueOf dataTypeOption

instance FindSTV One Int where
  specificTypeValueOf _ = 5

instance FindSTV Two Text where
  specificTypeValueOf _ = "another test"

instance FindSTV Three Decimal where
  specificTypeValueOf _ = 20.0

testTestFunc = script do
  [(testFunc One).age, (testFunc Two).age, (testFunc Three).age]
    === [16, 16, 16]
  (testFunc One).specificTypeValue === 5
  (testFunc Two).specificTypeValue === "another test"
  (testFunc Three).specificTypeValue === 20.0
  {- does not compile, due to type error
    • Couldn't match type ‘Int’ with ‘Text’
        arising from a functional dependency between:
          constraint ‘FindSTV One Text’ arising from a use of ‘testFunc’
          instance ‘FindSTV One Int’ -}
  -- (testFunc One).specificTypeValue === "hello"
1 Like

As an arguably simpler option, it looks to me like you may be able to get away with plain old union types here.

Let’s first look at your current attempt, because it’s actually going in a viable direction (though it’s not quite there yet).

  data AllDataTypes = AllDataTypes with
    dataType1: Optional DataType1
    dataType2: Optional DataType2
    dataType3: Optional DataType3

There are two main issues with this approach:

  • If, as you’ve mentioned, you have many fields in common, you’ll need to repeat those fields in each DataTypeX entry.
  • This representation makes it possible to have two (or even three, or even zero) values at the same time, and then what? Sure, you can try to write your code such that it never happens, but that’s hard, and will require a lot of discipline.

If you think of how you’d use that data type, all of your functions will look something like:

useAllDataTypes : AllDataTypes -> Either Text Int
useAllDataTypes adt = case adt of
  AllDataTypes (Some t1) None None -> Right 1
  AllDataTypes None (Some t2) None -> Right 2
  AllDataTypes None None (Some t3) -> Right 3
  AllDataTypes (Some t1) (Some t2) None -> Left "error"
  AllDataTypes (Some t1) (Some t2) (Some t3) -> Left "error"
  AllDataTypes None (Some t2) (Some t3) -> Left "error"
  AllDataTypes None None None -> Left "error"
  AllDataTypes (Some t1) None (Some t3) -> Left "error"

where you have to find which of the three is set, and then maybe cover the cases where multiple ones or zero are set. You wanted three options, and suddenly you have to contend with eight.

Instead of relying on human discipline, you can enlist the compiler to help you. Specifically, you can instead build the type in such a way that there can only be one value at a time:

data ExactlyOneDataType
  = Type1 DataType1
  | Type2 DataType2
  | Type3 DataType3

Using this becomes a lot easier:

useExactlyOneDataType : ExactlyOneDataType -> Int
useExactlyOneDataType eodt = case eodt of
  Type1 t1 -> 1
  Type2 t2 -> 2
  Type3 t3 -> 3

Note how we do not need to decide what to do with invalid cases, or use a compound return type to allow for errors: we know by construction that the output can only be exactly one of the types we expect.

Constructing it is also safer, as you cannot accidentally set more than one (or zero) values: you always have exactly one.

This also extends naturally to having lots of common fields:

data WithCommonFields = WithCommonFields with
  commonField1 : Int
  commonField2 : Int
  commonField3 : Text
  specificFields : ExactlyOneDataType

Now, there is only one type that will appear in most of your function arguments and return: WithCommonFields. Functions that only deal with common fields can ignore the specificFields field entirely and just “pass it through”. Functions that do need special behaviour based on that field can still do it:

useWithCommonFields : WithCommonFields -> Int
useWithCommonFields wcf = case wcf.specificFields of
  Type1 t1 -> 1
  Type2 t2 -> 2
  Type3 t3 -> 3

You can also mix and match:

data Type = One | Two | Three

extractNameAndType : WithCommonFields -> (Text, Type)
extractNameAndType wcf =
  let t = case wcf.specificFields of
        Type1 t1 -> One
        Type2 t2 -> Two
        Type3 t3 -> Three
  in (wcf.commonField3, t)
1 Like

Thanks @Gary_Verhaegen I eventually implemented the same thing as you advised already with a small change. I set a data structure with all common fields. And, all 3 different data types have the “WithCommonFields” member as a field inside.
So, I would call one time a function to set the WithCommonFields, and inside a case statement I will set all the specific fields for each one of them.