How to use [JS|Belt.Set|Belt.Map]Dict?

Hello.

I need to have a Dict with int keys and int values (want to count number appearances).

I see there is Js.Dict that does not have a method to see if specific key is in dict.

I also see that Belt.Set.Dict and Belt.Map.Dict (both appear in the docs as SetDict and MapDict which is again confusing) do have get but they need a comparator function.

(1) Is there a reason there is no generic Dict in Belt?

Eventually I thought using Js.Dict where the keys should be string.

I wanted to start with an empty Js.Dict:

let dict = Js.Dict.empty

and check/add values:

let keyOption = Js.Dict.get(dict, "1")
let newDict = Js.Dict.set(dict, "1", 0)

and for both line I got an error:

This call is missing an argument of type unit

After some time I saw the signature of the Js.Dict.empty is

let empty: unit => t<'a>

seems like it returns a function (2) why? The the docs say (and I would expect it to be so):

“Returns an empty dictionary.”

Adding () to the get/set function seems to get rid of the error, but the code is inside Array.reduce() and next time the dict will not be empty…

my function:

let countDigits = (str) => {
    let digits = str->Js.String.split("")
    digits->Belt.Array.reduce(Js.Dict.empty, (accDict, digit) => {
        let currentCount = accDict->Js.Dict.get(digit)
        switch currentCount {
        | Some(count) => accDict->Js.Dict.set(digit, count + 1)
        | _ => accDict->Js.Dict.set(digit, 0)
        }
    })
}

https://rescript-lang.org/try?code=DYUwLgBAxg9grgOzAEQJYHNVgM4QLwQAU2YATgJT4B8EA3gFARMSiQAmGWuBJpAtFQBS2AHQBlMqgToR2AA7AshAETLyjZh0w4BAIRDAwIgIKlSAQwCeI0iDZwoIQsJFooRkAFs5YSwBoicygoNzAArSxKPBoGZjiWcGg4MxAkAGF4JHwIIJDUdwEXUJF0cEIIsHV45mwAdywoAAsklPTMyFjqiAAfCDEYTydYRErqHODQwtFi7DKKgOGsgGoIAEYqrt6AfTHcyaFp-KNZsHLOMIgABg3qgF8NJlv1e6A

(2) How do I solve this?

Thank you in advance.

Hi, you originally wanted a dictionary with int keys and int values, right? So it seems Belt.Map.Int is the perfect fit: https://rescript-lang.org/docs/manual/latest/api/belt/map-int

Do you need specifically something else?

Thank you @yawaramin.

Using Belt.Map.Int and it compiles.

I would never look for Map while I need a Dict. Maybe because I saw Dict in another languages, but also because I see Dict in the docs.

Here I encountered into duplication of implementations and terms again - many Dict-s and Map-s which also behave like Dict-s.

My question is - why Js.Dict.empty returns something else that is not actually Js.Dict.t?

And as a suggestion for docs:

  • it would be so much easier to find things if the entries would be alphabetically sorted.
  • Seeng MapInt in docs I would search for Belt.MapInt which does not exist. Seems like there should be another level of nesting or at least written as Map.Int.

Thank you again.

1 Like

Your example didn’t compile because you need to return the modified dict value as the last expression of your reduce.

https://rescript-lang.org/try?code=DYUwLgBAxg9grgOzAEQJYHNVgM4QLwQAU2YATgJT4B8EA3gFARMSiQAmGWuBJpAtFQBS2AHQBlMqgToR2AA7AshAETLyjZh0w4BAIRDAwIgIKlSAQwCeI0iDZwoIQsJFooRkAFs5YS4XIANETmUFBuYEFaWJR4NAzMCSzg0HBmIEgAwvBI+BAhYajuAi7hIujghFFg6onM2ADuWFAAFilpmdmQ8bUQAD4QYjCeTrCI1dR5oeHFoqXYFVVBozkA1BAAjDU9-QD6E-nTQrOFRvNglZwREAAMWwkAvhqJBydPEPfqj0A

1 Like

Js.Dict.empty is a function that expects one argument: (). So if you call it like this: Js.Dict.empty(). You get back an empty Js.Dict.t.

1 Like

To expand on yaramin’s answer:
The Js.Dict api is mutable. Calling Js.Dict.set on a dict will mutate the underlying js object.
Therefore if Js.Dict.empty were a value (just an empty js object), every occurrence would reference exactly the same object.
To circumvent this empty is a function returning a new empty Dict.

2 Likes

@ryyppy I second that. - At least in the api docs this would make a lot of sense to me.

The difference is in the underlying implementation and their performance characteristics:

  • Js.Dict provides an api to work with js objects like you would do in js. The only restriction in rescript is, you can’t use polymorhpic objects with this api.
  • Belt.Map is a very performant generic implementation of a map structure. This introduces some minimal runtime “overhead” traded for different performance characteristics
  • Belt.MapInt is a provided default paramerization of Belt.Map due to it’s frequency of usage. - the same is true for Belt.MapString

While the structures and types are very similar they have different dis-/advantages and performance characteristics.

1 Like

I created an issue here.

Feel free to create issues on the rescript-lang.org repo for this kind of stuff!

3 Likes

Thank you @ryyppy and @woeps.

The things that I missed were:

  • Js.Dict is mutable and for Js.Dict.set there is a note at the end of the function description.
  • I need to add () for functions expecting unit, like for Js.Dict.empty(). This is a little confusing given the fact that in many other cases, especially when piping functions, the parenthesis are not needed: number->Belt.Int.toString / number->Js.Int.toString.

Thank you again for the detailed explanations.

We should probably put in the Js.Dict module description that this is a mutable representation of a JS object…

That is an inconsistency we sadly have to live with right now.
Rule of thumb: You always need to provide parenthesis for a function call, but you may need () for pipe calls, depending on the arguments.

It makes sense when you think about how -> is being interpreted:

"test"->Js.String2.toUpperCase

Is essentially transformed into

Js.String2.toUpperCase("test")

If you’d do "string"->Js.String2.toUpperCase() it would turn into Js.String2.toUpperCase("test", ()) which is wrong from an arity perspective.

We can’t special case toUpperCase(), because ReScript is a curried language with possible optional labeled arguments.

For example, whenever your last argument would be an optional label, you’d need to provide a final () to tell the compiler that we are applying the function.

let greet = (str, ~name=?, ()) => {
   switch(name) {
    | Some(name) => Js.log(str ++ " " ++ name)
    | None => Js.log("")
  }
}

"Hello"->greet();

Here, the () makes sense because it transforms into greet("Hello", ()), telling the compiler we don’t want to curry the function (simply said).

1 Like