Decorators in DAML

Mod note: As of SDK 1.5.0 Scenarios have been superseded by the more powerful Daml Script. We now recommend using that for all purposes. For more information, and to learn how to use Script please check out @Andreas’ post on our blog.

Sometimes it makes sense to have a default value for non-mapped keys in a map. In Scala for example this pretty easy:

val fruits = Map(
  "Apples" -> 3,
  "Bananas" -> 5,
  "Cherries" -> 2
).withDefaultValue(0)

fruits("Dates") // Returns 0

This is done of course with the decorator pattern. Is this possible in DAML? How would one go about that?

The code below is what I came up with.

data MapWithDefault k v = MapWithDefault
  with
    underlying : DA.Map k v
    defaultValue : k -> v


withDefaultValue : v -> DA.Map k v -> MapWithDefault k v
withDefaultValue defaultValue = withDefault (const defaultValue)


withDefault : (k -> v) -> DA.Map k v -> MapWithDefault k v
withDefault defaultValue underlying = MapWithDefault underlying defaultValue


lookup : DA.MapKey k => k -> MapWithDefault k v -> v
lookup key map = fromOptional
  (map.defaultValue key)
  (DA.lookup key map.underlying)


defaultValueReturned : Scenario ()
defaultValueReturned = scenario do
  let fruits = withDefaultValue 0 $ Map.fromList [
          ("Apples", 3)
        , ("Bananas", 5)
        , ("Cherries", 2)
        ]

  0 === (lookup "Dates" fruits)

Now this is problematic, because this data structure is not compatible with the original one, which is the whole point of the decorator pattern.

Is this possible only with typeclasses?

1 Like

If you want a function that does different things depending on the data type, you’re right, you need a typeclass.

Fortunately, you’re not limited to what’s built in. You can make your own.

Here’s a Map class that wraps DA.Next.Map, and provides an abstract mapLookup function (named as such because there’s already a lookup in the Prelude):

class Map a k v where
  mapLookup : k -> a -> Optional v

instance DA.Next.Map.MapKey k => Map (DA.Next.Map.Map k v) k v where
  mapLookup = DA.Next.Map.lookup

We can then instantiate it for our MapWithDefault type:

data MapWithDefault k v = MapWithDefault
  with
    underlying : DA.Next.Map.Map k v
    defaultValue : k -> v

withDefaultValue : v -> DA.Next.Map.Map k v -> MapWithDefault k v
withDefaultValue defaultValue = withDefault (const defaultValue)

withDefault : (k -> v) -> DA.Next.Map.Map k v -> MapWithDefault k v
withDefault defaultValue underlying = MapWithDefault underlying defaultValue

instance DA.Next.Map.MapKey k => Map (MapWithDefault k v) k v where
  mapLookup k (MapWithDefault underlying defaultValue) =
    case DA.Next.Map.lookup k underlying of
      -- I really wish we had `<|>`
      Some value -> Some value
      None -> Some (defaultValue k)

And, of course, verify it works with a scenario:

testAbstractMapLookup = scenario do
  let daMap = DA.Next.Map.fromList [(1, "x"), (2, "y")]
  mapLookup 1 daMap === Some "x"
  mapLookup 3 daMap === (None : Optional Text)

  let mapWithDefault = withDefaultValue "q" daMap
  mapLookup 1 mapWithDefault === Some "x"
  mapLookup 3 mapWithDefault === Some "q"

Yeah, custom typeclasses are possible.

But that means had I done something like a “map with default” it could not be transparent, at least not right now. If I’m right, that only would be possible if there was already a typeclass and the current Map had an instance and all functions like lookup would have been defined in that typeclass.

By the way the whole point of my custom lookup was to avoid the Optional return type when there’s a sensible default.

Would something like this make sense in a DA.Next.Map.Total or something? :thinking:

1 Like

That’s fair, but then you really couldn’t abstract over lookups; the two functions return something different. It would be very strange if lookup returned an Optional v for DA.Next.Map.Map and a v for MapWithDefault.

Scala does provide two different functions, Map#apply, which returns a V or throws a NoSuchElementException, and Map#get, which returns an Option[V]. If you were to introduce an unsafe lookup function that called error on None, you could get the same behavior.

unsafeMapLookup : Map a k v => k -> a -> v
unsafeMapLookup k a = Optional.fromSome $ mapLookup k a

If that existed in DA.Next.Map.Total, I think it’d be fair to remove the “unsafe” part. :smiley:

Hi Tamas,

There’s no direct analogue to the Decorator pattern in DAML, because DAML is by-and-large nominally typed, like Haskell. You could use typeclasses to try and make up for that, but it’s a bit overkill for this problem and not very idiomatic.

This particular access pattern for maps is well captured by using a function, not a separate map type. If you have a particular “default” in mind, you can write the lookup function yourself. Or you can build it with a more generic lookupWithDefault function:

lookupWithDefault : (k -> v) -> k -> Map k v -> v
lookupWithDefault f x m =
    case Map.lookup x m of
        None -> f x
        Some y -> y

For example, if you want to return the original key by default, you can define:

lookupOrKey : k -> Map k k -> k
lookupOrKey = lookupWithDefault identity

I hope this lightweight solution is useful for you.

Best regards,
Sofia

4 Likes

I understand the two functions would collide and lead to confusion.

However, an “unsafe” lookup is not really appropriate either. The intention here is not make it unsafe, but to have data structure that can always return something meaningful.

I think the crux of the issue here is that what you want is subtyping, and that is just not something DAML has. It’s tempting to try and use type classes for that, but that’s not really what they’re meant for.

In DAML (as opposed to Scala), data structures are, they don’t do. Data structures don’t return things, functions do. I guess what I’m trying to say is there is no direct answer to your question because the conceptual model is different. I would encourage you to change the question to work with DAML concepts: have a lookup function that always returns something meaningful.

The answer to that is obviously yes, and quite simply, as @Sofia_Faro demonstrated.

Her answer is slightly more powerful than your Scala example; you could use

lookupOrZero = lookupWithDefault (const 0)

to match your original Scala example. Or rewrite lookupWithDefault to take a value instead of a function.

2 Likes