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:
A “normal” Daml function’s type has the form X -> Y -> Z where X and Y are argument types, and Z is the result type, all separated by -> But there are many definitions in Daml that have a different symbol, =>. Like so: C => Z where Z might have some -> of its own. It looks sort of like a function type, but most of the time the call of the function contains no reference whatsoever to C. So what is this thing? We call the part before the => a constraint, or typeclass constraint in common par…
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.
At the very end of episode 2 of our new Introduction to Daml, @Levente_Barczy mentions that serious Daml projects will use custom record types extensively. I think Daml developers should consider adding records to their repertoire as soon as possible, because the structure of templates lends itself to an especially direct use of records. Records and their templates Every record is exactly as powerful as the fields in it, no more, no less. That means that if you have three fields in a templat…
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"
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
DataTypeXentry. - 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)
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.