Custom bounded types

Is it possible to create custom, bounded types in Daml? For example:

template Example
  with
    party : Party
    ageLimit : Bounded 18 65
  where
    signatory party

where ageLimit argument would be limited to Ints between 18 and 65.

I have found the Bounded typeclass, but not sure how to use it and if that is applicable to this use case.

1 Like
module Main where

import Daml.Script

newtype MyBounded = MyBounded Int deriving (Show, Enum)

instance Bounded MyBounded where
  minBound = MyBounded 18
  maxBound = MyBounded 65

setup : Script ()
setup = script do
  debug $ ([minBound..maxBound] : [MyBounded])
  return ()

prints

[MyBounded 18,MyBounded 19,MyBounded 20,MyBounded 21,MyBounded 22,MyBounded 23,MyBounded 24,MyBounded 25,MyBounded 26,MyBounded 27,MyBounded 28,MyBounded 29,MyBounded 30,MyBounded 31,MyBounded 32,MyBounded 33,MyBounded 34,MyBounded 35,MyBounded 36,MyBounded 37,MyBounded 38,MyBounded 39,MyBounded 40,MyBounded 41,MyBounded 42,MyBounded 43,MyBounded 44,MyBounded 45,MyBounded 46,MyBounded 47,MyBounded 48,MyBounded 49,MyBounded 50,MyBounded 51,MyBounded 52,MyBounded 53,MyBounded 54,MyBounded 55,MyBounded 56,MyBounded 57,MyBounded 58,MyBounded 59,MyBounded 60,MyBounded 61,MyBounded 62,MyBounded 63,MyBounded 64,MyBounded 65]
1 Like

Note that this does not enforce any bounds. MyBounded 15 will compile and run just fine. Enforcing those invariants can only be done indirectly via ensure clauses at the moment. See Should runtime/API respect namespace scopes? and Extending Ensure for non Templates - #4 by Gary_Verhaegen for some discussion.

1 Like

Though note that debug (MyBounded 1) also works, so this may not be what you want here.

1 Like

Paraphrasing what everyone has said above, I think Bounded just provides you with functions that return the min and max of a type, which is useful for syntactic sugar like [Jan .. Jul]. As the other contributors said, it doesn’t actually enforce these bounds at the type level.

1 Like

Actually I think my example was wrong in the first place. So what I’m trying to do is something like this:

template Example
  with
    party : Party
    a : Limited Int
    b : Limited RelTime
  where
    signatory party

And when such a contract would be create, the party would able to specify the actual limits, like:

Example with party, a = limited 18 65, b = limited (hours 2) (days 30)

where limited could be a smart constructor.

I deliberately avoided to use Bounded to indicate I’m okay to create something completely different if needed.

Well, I guess I can create my own data type for that. So I think the lesson here is that 1) there is nothing built-in for that, 2) I cannot have compile-time safety due the nature of the problem.

1 Like

Would a simple data Range a = Range with inf : a, sup: a work? What do you need the smart constructor for?

1 Like

That’s actually what I have. Although I think I’ll change the name, but that’s irrelevant.

I wanted a smart constructor to prevent clients to use the data constructor and to keep the internals hidden. It’s all in flux though…

1 Like

I think this is where you go wrong:

As the discussions @cocreature pointed to explain, there is no such thing as a smart constructor in Daml. There is also no way to hide a constructor at the LF level.

This is not about compile-time safety, this is about push-to-the-ledger-time safety.

If you want trusted validations, you have to have a trusted party validating the data. I think this could be a good use-case for triggers: you create your contract with the trigger party as an observer, and the (trusted) trigger runs the validations and signs your contract.

2 Likes

Just for the sake of argument how is this achieved in the standard library? For example RelTime cannot be constructed using the data constructor, just via certain functions. Does -- | HIDE have special meaning?

newtype RelTime = -- | HIDE
  RelTime with microseconds : Int
1 Like

-- | HIDE only hides it from the documentation, it has no semantic effect.

daml-stdlib doesn’t solve this problem. If you have a RelTime field on a template you can send whatever you want over the ledger API and no validation will take place. That’s what the thread I linked above discusses Should runtime/API respect namespace scopes?.

We do try to make it hard to accidentally construct it manually so directly in Daml you’ll have a hard time (I expect you can bypass the checks if you try hard enough but it’s not trivial):

  1. The actual type definition is in DA.Time.Types, DA.Time only reexports the type but not the constructor.
  2. The compiler has a hardcoded list of internal modules that you cannot import without getting an error.
  3. DA.Time.Types has special treatment in data-dependencies which means you also cannot directly bypass it that way.