Destructuring tuple in array map does not work. Confused by error

Why don’t the first two examples below compile? Actually it looks like the second one compiles but if you comment out mapA you’ll see the same error on mapB. The error is…

let mapA = [(1, "one"), (2, "two"), (3, "three")]->Js.Array2.map((n, s) => s)
let mapB = [(1, "one"), (2, "two"), (3, "three")]->Belt.Array.map((n, s) => s)

let mapC = [(1, "one"), (2, "two"), (3, "three")]->Js.Array2.map(i => {
  let (n, s) = i
  s
})

The error is…

This expression’s type contains type variables that can’t be generalized:
Js.Array2.t<’_weak1 => '_weak1>. This happens when the type system senses there’s a mutation/side-effect, in combination with a polymorphic value. Using or annotating that value usually solves it.

I couldn’t figure out how to annotate it to make the problem go away. mapC seems to work.

Figured it out. Needed more parenthesis:

let mapA = [(1, "one"), (2, "two"), (3, "three")]->Js.Array2.map(((n, s)) => n)

I got bitten by this again. Very confused about why so many parenthesis are needed. Look at this code. flattenA works. I want flattenB to work and it seems like it should but it doesn’t. Why are so many parenthesis needed? If I mouse over x the tooltip is (string, array<string>). This seems odd. But worse is if I mouse over yy it shows array<'a>. If x completely consumes the mapped element, then why is yy even possible? Shouldn’t I get an error saying something like Map requires one input parameter but you're giving it two. Doesn’t flattenB just look like it should work with the number of parenthesis used?

let synonyms = [
  ("happy", ["glad", "joy", "glad"]),
  ("sad", ["unhappy", "dejected", "somber"]),
  ("angry", ["enraged", "furious"]),
]

let flattenA =
  synonyms->Belt.Array.map(((a, bb)) => bb->Belt.Array.map(b => (a, b)))->Belt.Array.concatMany

let flattenB = synonyms->Belt.Array.map((x, yy) => yy->Belt.Array.map(y => (x, y))) // can't complete with concatMany

Why are so many parenthesis needed? If I mouse over x the tooltip is (string, array<string>) .

When you use (x, yy) your function expects two arguments.

When you use ((x, yy)) your function expects one argument which is a tuple, and this syntax is destructuring the tuple.

But worse is if I mouse over yy it shows array<'a> . If x completely consumes the mapped element, then why is yy even possible?

I suspect when you pipe yy into Belt.Array.map then ReScript infers that its type is array<'a>.

2 Likes

This is exactly the same in JS/TS where you might call:

.map((element, index) => { ... })    /* Javascript */

It naturally assigns the second parameter in the parens as the index parameter, so you have to more explicitly destructure the tuple (by using additional parentheses) if you want to distinguish between your second argument being your indexer or the second argument to your destructured tuple.

.map(((tupleItemA, tupleItemB), index) => { ... })    /* Javascript */

In ReScript it’s exactly the same except the indexer arg comes first in mapWithIndex:

let arr = [(1, "one"), (2, "two"), (3, "three")]
 
arr -> Belt.Array.map(((tupleItem1, tupleItem2)) => { ... })
arr -> Belt.Array.mapWithIndex((index, tuple) => { ... })
arr -> Belt.Array.mapWithIndex((index, (tupleItem1, tupleItem2)) => { ... })

The explanation about mapping with index makes sense to me. With the mapIndex function the compiler is expecting a function with two arguments, the second of which is an integer. And maybe it looks like I’m trying to pass a function with two arguments to the map function when what I want is to pass a function with one argument that is a tuple. Part of what is confusing me is that I come from experience with F#. This works as I expect and I don’t understand why ReScript doesn’t do it the same way.

\\ F# 
let data = [("one", 1); ("two", 2); ("three", 3)]
let x = data |> List.map(fun (i,j)-> i)
let y = data |> List.map(fun i -> i)

In ReScript below the x becomes an array of snd->(string, int). I see why this is happening but it is somewhat unintuitive.

\\ ReScript
let data = [("one", 1), ("two", 2), ("three", 3)]
let x = data->Array.map((i, j: 'snd) => i)

The ReScript and F# samples look the same to me.

F#: fun (i, j) -> i extracts the first element of a tuple. fun i -> i returns the tuple (or whatever i is)` unchanged.

ReScript: (i, j) => i extracts the first element of the tuple. i => i would return the tuple unchanged.

These are purely syntactic differences, the meaning is the same.

EDIT: whoops, after reading @amiralies 's comment I now realize I made the same mistake you did! :smiley: They are indeed different because of the reason he explained.

fsharp/ml function signature syntax is different from rescript’s

in imaginary syntax notation:
ml/fsharp is like

arrowGrammar ::= "fun" pattern+ "->" expr

andrescript

arrowGrammar ::= (name | "(" pattern* ")") "=>" expr

so the parenthesis-less syntax is an special case which only works for names (identifier / underscore)