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?
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.
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