Compile error using fast pipe operator after pipe last in ReasonML
Asked Answered
N

2

3

The way that the "fast pipe" operator is compared to the "pipe last" in many places implies that they are drop-in replacements for each other. Want to send a value in as the last parameter to a function? Use pipe last (|>). Want to send it as the first parameter? Use fast pipe (once upon a time |., now deprecated in favour of ->).

So you'd be forgiven for thinking, as I did until earlier today, that the following code would get you the first match out of the regular expression match:

Js.String.match([%re "/(\\w+:)*(\\w+)/i"], "int:id")
|> Belt.Option.getExn
-> Array.get(1)

But you'd be wrong (again, as I was earlier today...)

Instead, the compiler emits the following warning:

We've found a bug for you!
OCaml preview 3:10-27
This has type:
  'a option -> 'a
But somewhere wanted:
  'b array

See this sandbox. What gives?

Nysa answered 2/3, 2019 at 10:37 Comment(0)
M
4

Looks like they screwed up the precedence of -> so that it's actually interpreted as

Js.String.match([%re "/(\\w+:)*(\\w+)/i"], "int:id")
|> (Belt.Option.getExn->Array.get(1));

With the operators inlined:

Array.get(Belt.Option.getExn, 1, Js.String.match([%re "/(\\w+:)*(\\w+)/i"], "int:id"));

or with the partial application more explicit, since Reason's syntax is a bit confusing with regards to currying:

let f = Array.get(Belt.Option.getExn, 1);
f(Js.String.match([%re "/(\\w+:)*(\\w+)/i"], "int:id"));

Replacing -> with |. works. As does replacing the |> with |..

I'd consider this a bug in Reason, but would in any case advise against using "fast pipe" at all since it invites a lot of confusion for very little benefit.

Moll answered 2/3, 2019 at 11:10 Comment(11)
Thanks! Your answer helps a lot (so does your cookbook :) ). There appear to be quite a few mentions of this in the repo issue tracker. This one suggests using parens a little too liberally for my liking. What's your suggestion for coding style that avoids pipe first? Using the pizza pipe with _ positioning? Or just use intermediate let bindings and embrace the imperative feel? (I realise that's a matter of opinion, hence asking in the comments! Just interested what your personal pref is).Nysa
|> _ can be used reliably in place of fast pipe I think, but I'd try to avoid "t first" functions entirely, which means ditching Belt. Functions designed with t first also doesn't work well with partial application, which is essential for effective function composition and a large part of what makes a language functional.Moll
It seems like Reason is trying to push conventions that deemphasize the functional nature of the underlying language in favor of idioms from JavaScript. But since the ecosystem and even most of the functionality that ships with the compiler is based on the old conventions it ends up as a very confusing mess. And there seems to be little interest in addressing, or even acknowledging these issues. Belt and the fast pipe was introduced over a year ago with promises that everything would be explained, but very little has been said about it since.Moll
But with _ positioning you can achieve the partial applications one might desire, surely? There is the expense of extra punctuation and mental overhead of course.Nysa
I've definitely found the Reason APIs confusing. They seem to suffer from an overabundance of ways to do things (e.g. best way to make a Map). Which would by itself be undesirable, but one must also choose wisely because there are plenty of gotchas. The lack of official docs that go beyond introductory level really hurts once you start building out code. Hopefully my company buys in and is prepared to sponsor some time contributing to documentation / tutorials.Nysa
Here's an example of what I mean with partial application of t first vs t last: In t first, if you want to List.map over a function like Belt.Option.getWithDefault you have to do things |> List.map(thing => thing |> Option.getWithDefault(_, "foo")). With t last you can get away with just List.map(Option.getWithDefault("foo")). And while this example might seem trivial, it quickly adds up if you do a lot of function composition in this way.Moll
To take it another step, if you want to make this example into a function, and List.map is also t first, you would have to write let defaultMap = things => things |> List.map(_, thing => thing |> Option.getWithDefault(_, "foo")), while with t last it would just be let defaultMap = List.map(Option.getWithDefault("foo")).Moll
Eww yes that is nasty. I was under the perhaps mistaken impression that you could go things |> List. map(Option.getWithDefault(_, "foo")) (or let defaultMap = List.map(Option.getWithDefault(_, "foo")) for the function definition). But reasonml.chat/t/partial-application-and/714 does seem to cast some doubt on that... must try it on the sandbox when I get to my desktop. The statement "we might remove this in future" doesn't inspire confidence.Nysa
Getting a bit OT in here perhaps ... but what do you make of Chenglou's readability argument? I can somewhat agree that it might be confusing to newcomers, but only if they haven't made the functional shift yet.Nysa
Ah, yes I forgot that works. And I agree it's not very readable, even to experienced reasoners because it overloads the meaning of _ and has issues with nesting. It would have been better with a separate and more searchable placeholder (Kotlin for example uses it). And in that case I think it would be friendlier to newcomers than currying. Unless you start nesting it, in which case it still becomes confusing to have multiple its referring to different things. Currying is conceptually much simpler and more flexible, but unfortunately not very discoverable.Moll
I feel like the meaning of "computer, I don't need this symbol" and "computer, you don't need this symbol" are close enough to work with :) I'm ok with scattering my code with placeholders, but agree that searchability should have been a higher priority. Especially, as you say, given that it can change the semantics of an expression so dramatically.Nysa
N
1

Also see this discussion on Github, which contains various workarounds. Leaving @glennsl's as the accepted answer because it describes the nature of the problem.

Update: there is also an article on Medium that goes into a lot of depth about the pros and cons of "data first" and "data last" specifically as it applies to Ocaml / Reason with Bucklescript.

Nysa answered 3/3, 2019 at 2:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.