Open ospencer opened 4 years ago
We could totally just implement this as let (|>) = (a, b) => b(a)
, but we should probably have an optimization for it if we do that. I love the idea of having this defined as a function, though.
The alternative is that this is just syntactic sugar.
I would be up for working on this if you're cool with it 😄
I'm actually up to this right now as I'm implementing curry
! My bad for not assigning this to myself!
ah ok, no worries
If this gets implemented we should also address #671
Right now the |>
doesn't really fit into the stdlib.
It's not possible to write something like [1, 2, 3] |> List.reverse |> List.map(fn)
because map
takes the list as second argument and not as the first one.
For this operator to actually feel like part of the language it has to be compatible with the stdlib where possible.
Any progress on adding this to the language together with a data-first approach and maybe curry placeholders? This would really aid a lot in reading and writing Grain code :-)
if it is not possible to implement the pipe operator after changes with data-first, would it be possible to implement "Uniform Function Call" like nim?
the above example would look
[-1, -2, 0, 1, 2].filterNegatives().doubleValues()
I don't know if it's possible to implement uniform function call with type inference (like hindley-milner)
@tredeneo The idea of the pipe operator is to allow for the same functionality as "Uniform function calls" so it wouldn't make sense to have both. It also shouldn't be a problem for HM type inference as it's just an alternate way to apply arguments to a function. ReScript has a feature closer to what you're talking about for instance https://rescript-lang.org/docs/manual/latest/pipe
Currently at 0.5.13, if I have the following definitions:
let add = a => b => a + b;
let (|>) = (a, f) => f(a)
then this syntax works:
let x = 4 |> add (5)
and this syntax works:
let x =
4 |>
add 5
but syntax error is thrown when the operator begins a new line:
let x =
4
|> add (5)
Yes, binary operators in Grain must appear on the same line as the first argument.
Sure, and there's no technical problem with it staying like that. For some binary operators maybe it seems natural to put the operator at the end of the line, but coming from other functional languages I see that its more conventional and natural to put the pipeline operator starting on new line. Even in the example program you shared you started the new lines with the pipeline operator, so I think Grain should support that syntax too.
Just calling out that it's not a bug 🙂
Supporting that syntax would be an entirely separate issue from this, though. Because an end-of-line character terminates a statement in Grain, it's non-trivial to support, and probably something we won't be able to support until a full parser rewrite.
Cool! I certainly think it would be a boon for Grain, just one less thing for newcomers like me from other functional languages to get quirky over.
I've only gotten started with Grain in the past few days and love it so far. I appreciate all your hard work and hopefully can contribute someday when I get more comfortable with parsers and lexers and such.
I will say though, if it doesn't become baked into the language, I still do like the ability to define |>
however I like. It means I can "overload" it whatever functionality I want to in a given pipeline.
just stumbled across Grain and it's such a nice language! I also really admire the goal. I wanted to add my two cents here and put another vote in for the "Uniform call syntax" (wikipedia) mentioned by @tredeneo. Here are some of the primary reasons I believe it would be a great choice for Grain:
.
and expect to see a list of methods/fields. Not quite as smooth an experience when using pipesList.map(x => x * 2)
, the code reads left to right, which is much more intuitive than reading "inside out"cow.eat("grass")
) which is how English is structured (code is written in English for better or worse)myMap.set(a, b)
vs Map.set(a, b myMap)
)Anyways, I put together this little example to get a feel for what it'd looks like and add some more context/examples:
import List from "list"
import Map from "immutablemap"
// current awkward way to get dot call syntax without using modules.
// Goes against the functional grain
record Database<item> {
getStore: () -> Map.ImmutableMap<Number, item>,
insert: (Number, item) -> Result<String, String>
}
// good! Keeps data and functions separate
record Person { id: Number, name: String, age: Number }
// currently awkward and inconsistent to use these since they can't be called with dot call syntax
let greet = (person: Person) => List.join("", ["hello ", person.name, "!"])
let save = (person: Person, db: Database<Person>) => db.insert(person.id, person)
// db impl is awkward. The db instance needs to reference another store instance instead
// of the methods being self-contained
let mut store = Map.fromList([])
let db = {
getStore: () => store,
insert: (id: Number, person: Person) => {
if (Map.contains(id, store)) {
Err("person already exists")
} else {
store = Map.set(id, person, store)
Ok("saved person")
}
}
}
// forced to use different calling conventions depending on where/how the function was defined
let johnny = {id: 0, name: "Johnny", age: 33}
print(db.getStore())
print(save(johnny, db))
print(db.getStore())
print(save(johnny, db))
import List from "list"
import Map from "immutablemap"
// good! Keeps data and functions separate
record Person { id: Number, name: String, age: Number }
// "class methods" can be split out into multiple files instead of accumulating in a record type
let greet = (person: Person) => "".join(["hello ", person.name, "!"])
let save = (person: Person, db: Database<Person>) => db.insert(person.id, person)
// the more complex database example now doesn't need an extra layer of nesting, which helps keep code cleaner
record Database<v> { mut store: ImmutableMap<Number, v> }
// technically could clean up and don't even need this method anymore
let getStore = (db: Database<v>) => db.store
// easier to refactor and make this generic in the future to support types other than Person
let insert = (db: Database<Person>, id: Number, person: Person) => {
if (db.store.contains(id)) {
Err("person already exists")
} else {
db.store = db.store.set(id, person)
Ok("saved person")
}
}
let db = { store: Map.fromList([]) }
// calling code can be more consist in its convention and use dot calls or function calls as desired for readability
let johnny = {id: 0, name: "Johnny", age: 33}
print(db.getStore())
print(johnny.save(db))
db.getStore()
.get(0)
// .print() // easy to comment out pipeline steps when debugging
save(johnny, db).print() // no editor gymnastics needed to quickly add a println
// all normal method calls still work
insert(db, 1, { id: 1, name: "Jimmy", age: 34 })
Anyways, thanks for taking the time to consider this (and for all the hard work on Grain so far)!
@jsnelgro thanks for the thorough feedback! I'm personally a fan of this feature in other languages that support it but I think that the current features in Grain provide a hostile environment for the existence of this syntax, and supporting it would require some significant overhauling of the language to make it convenient to use. Some thoughts:
String
for string functions) rather than in the "globally imported" Pervasives, which is intentionally kept minimal for only core/common functions like print
, (+)
, etc. To the point of making a language that is ergonomic to developers coming from OOP languages, someone may be confused why "abc".length()
does not work in a vacuum. Since the length
function for a string rather exists in the String
module, you would either first have to do something like import { length } from "string"
to bring the function into the context of your file or have a way to explicitly indicate where the length
function lives, perhaps something like "abc".String.length()
. In either case, this extra boilerplate and a mental leap into Grain's way of doing things would already be required if coming from an OOP language, and it may be disappointing to not have what you were looking for show up in autocomplete when typing .
on a string.let myFunc = () => print("first")
myFunc() // will print "first"
let myFunc = () => print("second") // this definition overwrites/shadows the previous one
myFunc() // will print "second"
Let's say from the first point you were OK with doing import { length } from "string"
to be able to do "abc".length()
. This may be fine until you also want to use this syntax for an array, in which case you might also do import { length } from "array"
. However, now you have overwritten the definition of the length function to be an array's length, so "abc".length()
would no longer work.
The |>
syntax definitely may look weird to an OOP programmer, though I personally don't think it would be too big of a hurdle to cross; there's also a proposal to add this syntax into JS https://github.com/tc39/proposal-pipeline-operator, so it may become more commonplace in the future. I also think that the points you bring up of left-to-right reading and subject-verb order would be alleviated by a |>
operator, just with a different syntax.
Ah, these are all great points. Uniform call syntax doesn't work well without function overloading. In your string example, I guess you'd end up with one big pattern matching function that took a union type of everything that could have a length
. Aside from being messy, it'd also be impossible for library clients to extend it.
The imports issue doesn't seem too bad since most IDEs can figure out the imports for you (Kotlin extension methods in IntelliJ work like this and it's a nice DX).
Given the language constraints, I agree the pipe operator makes more sense. And it's not too big of a mental leap to figure it out. Hope it shows up in JS soon 🤞
Is this feature being currently worked on (last comment being almost a year ago)?
Is this feature being currently worked on (last comment being almost a year ago)?
Currently this still does not exist in the language though you can define it as:
let (|>) = (a, f) => f(a)
We even expose this in the json
module for working with lenses currently
Without partial application (there is a pr for this open currently) and currying implemented just yet it wouldn't be the most useful as there would be no way to chain together a lot of the standard library functions, and as mentioned above we would probably want to look into having specific optimizations for this operation.
The reverse application operator (
|>
) (sometimes called the pipeline operator) takes two arguments and applies the second argument to the first argument. The type signature of|>
is(a, a -> b) -> b
. It is left-to-right associative and of this snapshot of our operator precedence document it would have operator precedence 45.Here's an example program: