Open dead-claudia opened 5 years ago
I think that this should be a separate proposal, with a different token (e.g. ?>
)
I'm not tied to the syntax - I was just using it as an example. Admittedly, it's a bit ugly.
I think that this should be a separate proposal
It seems orthogonal in the way private fields are orthogonal to public fields, so I'm not convinced.
Given that bar
wouldn't be available there, and for the placeholder case, you can do |> # ?? doSomething(#)
, it seems like the need only arises in the form that has no placeholder?
@ljharb That would execute doSomething
only if #
is nullish, which I think is the opposite of what's intended here.
@mAAdhaTTah then # || doSomething(#)
or # == null ? # : doSomething(#)
, either way no additional syntax support would be needed.
Feasibly that works with F# as well, just needs to be wrapped in an arrow function.
Regardless though, I'm also inclined to think this should be a separate, follow-on proposal rather than overloading the initial operator design.
This would really bloat the proposal and it can be done in a separate proposal after this one has already been promoted to stage3. Does seem useful though.
Happy to support anyone who wants to make a new proposal for this. Typical real-world usage example:
Example usage:
function loadFromCrossSessionStorage(): SavedData {
return (window.localStorage.getItem(DATA_KEY) ?> JSON.parse) ?? DEFAULT_DATA;
}
Compare with required code:
function loadFromCrossSessionStorage(): SavedData {
const savedData = window.localStorage.getItem(DATA_KEY);
if (savedData != null) {
return JSON.parse(savedData);
}
return DEFAULT_DATA;
}
Compare with @ljharb's suggestion:
function loadFromCrossSessionStorage(): SavedData {
return (window.localStorage.getItem(DATA_KEY) |> # != null ? JSON.parse(#) : #)
?? DEFAULT_DATA;
}
Just my personal opinion, but I think this usage is getting just a bit too tricky. I worry that if we start adding many cute combinations like this, it will get hard to read JavaScript code.
@littledan While I agree in general and I plan specifically not to suggest any further, I feel this one, optional pipeline chaining, to be generally and broadly useful enough to merit a single exception. This is about as far as I'd be okay with, and I agree anything more is just superfluous and unnecessary.
For comparison, here's it compared to a hypothetical String
prototype method equivalent to JSON.parse
:
function loadFromCrossSessionStorage() {
return (window.localStorage.getItem(DATA_KEY) ?> JSON.parse) ?? DEFAULT_DATA;
}
function loadFromCrossSessionStorage() {
return window.localStorage.getItem(DATA_KEY)?.readJSON() ?? DEFAULT_DATA;
}
Visually, it doesn't look that different, and semantically, the way a lot of people tend to use pipeline operators, it fits right into their mental model of them being "method-like".
Separately, for language precedent:
Most FP languages with this operator or equivalent (OCaml, Elm, PureScript, Haskell via f & x = f x
, etc.) use explicit option types, and so they just use monadic bind or similar for similar effect. Instead of doing x |> f
or use a special x ?> f
operator, they'd do x |> map f
, x |> Option.map f
, or similar, and their compilers virtually always compile that indirection away. This is mostly because they don't have nulls at all, and so they have no need for an operator like this. Instead, they just use a wrapper type that allows optional values. It's a similar story with Result<T, E>
/Maybe a b
types where they use those instead of try
/catch
(and process those similarly).
Here's @xixixao's example ported to OCaml + BuckleScript's Belt (a standard library optimized for compilation to JS):
open Belt
// DOM interop code omitted
let loadFromCrossSessionStorage () : SavedData.t =
window |> Window.localStorage |> Storage.getItem DATA_KEY
|> Option.flatMap (fn s -> Js.Json.parseExn s |> Js.Json.decodeObject)
|> Option.flatMap SavedData.decode
|> Option.getWithDefault DEFAULT_DATA
Swift, C#, and Kotlin all three support extension methods and a null coalescing operator, which works out to near identical effect to my proposal when used together.
Here's @xixixao's example ported to Kotlin, using an inline extension method:
import kotlin.js.JSON
import kotlin.browser.window
private inline fun String.readJSON() = JSON.parse(this).unsafeCast<SavedData>()
fun loadFromCrossSessionStorage(): SavedData {
return window.localStorage.getItem(DATA_KEY)?.readJSON() ?? DEFAULT_DATA
}
Rust uses methods instead of pipeline operators, and so it's more idiomatic to use those when you'd ordinarily use a pipeline operator. For optional values, opt.map(f)
is preferred, but there is the sugar foo.method()?
for match foo.method() { Some(x) -> x, None -> return None }
, which works similarly to foo?.method() ?? return nil
for an extension method in C# or Kotlin.
Here's @xixixao's example ported to Rust Nightly + stdweb, using a simple .and_then
to efficiently chain:
use stdweb::*;
fn load_from_cross_session_storage() -> SavedData {
web::window().local_storage().get(DATA_KEY)
.and_then(|json| js!{ JSON.parse(@{json}) }.try_into())
.unwrap_or(DEFAULT_DATA)
}
For stable Rust, change .try_into()
to .into_reference()?.downcast()
, what it's implemented in terms of.
Elixir appears to be an oddball that 1. features an explicit pipeline operator, 2. features a literal nil
value, and 3. doesn't have anything to chain nulls with. It also features no way to return early from a function (or anything that could provide similar functionality), so that limitation is pretty consistent with the rest of the language.
Here's a hypothetical translation of @xixixao's example to Elixir:*
require JSON
def load_from_cross_section_storage()
json_string = Js.global("window")
|> Window.local_storage()
|> Storage.item(DATA_KEY)
if json_string.is_null()
DEFAULT_DATA
else
JSON.decode!(json_string)
end
end
* I'm intentionally avoiding ElixirScript, as that doesn't appear to have much traction and has nowhere close to the level of support that the official Elixir compiler has, in contrast to cargo-web + stdweb for Rust, BuckleScript for OCaml, or ClojureScript for Clojure.
As for a language that doesn't support it, let's use Java as an example. Here's @xixixao's example translated to Java + GWT:
private static SavedDataFactory factory = GWT.create(SavedDataFactory.class);
public SavedData loadFromCrossSectionStorage() {
String json = Storage.getLocalStorageIfSupported().getItem(DATA_KEY);
if (json == null) {
return DEFAULT_DATA;
} else {
return AutoBeanCodex.decode(factory, SavedData.class, json).as();
}
}
If you have to repeat this logic in several places, you can imagine it'd get clumsy in a hurry.
So in summary, that simple operation is itself already pretty clumsy in languages that lack equivalent functionality, and it does in fact get harder to read IMHO the more often it appears. Personally, I find myself abstracting over this lack of functionality in JS a lot, and in a few extreme cases wrote a dedicated helper just for it.
@isiahmeadows
This is really a helpful analysis!
Personally I don't like ?|>
or ?>
because we never need such operator in mainstream fp languages, but as your comment, it just another symptom of mismatching between mainstream fp and javascript world (no option types).
With the examples of other languages, I am increasingly convinced that we may need a separate Extension methods proposal to solve this problem.
@hax
With the examples of other languages, I am increasingly convinced that we may need a separate Extension methods proposal to solve this problem.
Check out https://github.com/tc39/proposal-pipeline-operator/issues/55 - that covers the this
-chaining style (which is technically mathematically equivalent).
@isiahmeadows Thank you for the link.
Actually now there are 5 different strategies for a@b(...args)
:
b.call(a, ..args)
b(a, ...args)
b(...args, a)
b(...args)(a)
(F# style)a@b(x, #, y)
for b(x, a, y)
(Smart pipeline style)There are already many discussion between last two options (F# vs Smart style), so no need to repeat them here. What I what to say is, many use cases of first option can never be meet with option 4 and 5. Especially option 5 can cover all use cases of 2,3,4 but not 1.
I also think the main motivation of Extension methods is not method/function chaining, So I believe we can have both Extension methods and Pipeline op.
Return to this issue itself (optional chaining), it's strange to have special pipeline op for it, but I feel it's acceptable to have a?::b(1)
. And it also show us another important difference between Extension methods with Pipeline: Extension methods are more OO-style so programmers will also expect extension accessors (a?::x
which match a?.x
). From the Extension perspective (like Extension in Swift/Kotlin), it's actually strange to have a::b
work as b.bind(a)
, I feel many would expect b
as a getter/setter pair ({ get() {...}, set(v) {...} }
) and a::b
should work as b.get.call(a)
, a::b = v
should work as b.set.call(a, v)
. I would like to create an Extension proposal follow this semantic.
@hax I agree that (a?::b
as an extension property accessor) could be useful, but I'd rather keep that to a separate follow-up proposal and not particular to this bug.
@littledan I concur w/ @isiahmeadows this is definitely an exception & would be immensely useful for those who are most likely intend to write in this style, functional programmers or reactive functional programmers. It would save a tremendous amount of time having to normalize and wrap over methods like window.localStorage
to do data process pipelining of various lengths of depth efficiently.
I find using pipeline, I end up with tons of nullish checks. Lots of times, I really just want an Option, which just isn't that easy to work with in JS. Instead, we have null, and I think we should leverage that. To that end, I think if we build a proposal out of this, the operator should be ||>
, a visual indicator that we're really talking about a second rail. In particularly long pipelines it should be very easy to parse null handling by the reappearance of |>
after a series of ||>
.
It seems like support for optional chaining pipelining can be sort of cleanly separated from the rest of the pipeline proposal, and added as a follow-on. Is this accurate, or are there additional cross-cutting concerns?
It can be a follow-on - it already depends on the pipeline operator making it.
Has anyone taken to writing a proposal for this add-on?
I'm doing game dev and this would greatly clean up my code. I could write collision detection as such:
collidesHow(entityA, entityB) ?> onCollision)
So if the entities collide, then onCollision is called with how, otherwise nothing is called.
Without this addition, I'd have to unpackage the how:
collidesHow(entityA, entityB) ?> how => how && onCollision(how)
And without the pipeline proposal, I'm actually using map, then filter, then foreach to build up what a potential collision is, filtering it if it didn't collide, then calling onCollision if it did. It reads nicely, but is less effecient.
This might be going too far, but what about another extension using the array syntax:
collidesHow(entityA, entityB) ?> [storeCollision, onCollision]
which would be equivilent to:
const how = collidesHow(entityA, entityB);
if (!!how) {
storeCollision(how);
onCollision(how);
}
Note: this is because pipeline works by chaining and if storeCollision doesn't return how
, then we wouldn't be able to pipe it to onCollision.
How about:
const ifNotNull = f => a => a && f(a)
collidesHow(entityA, entityB) |> ifNotNull(onCollision)
Not sure what it does to performance but since ifNotNull(onCollision)
has to be resolved only once, and not every iteration, I think it couldn't hurt too much...
Also if you use ifNotNull
a lot, you could come up with a shorter name so it doesn't add that many extra characters.
Point-free is more ideal; or is that a suggestion for today prior to a
functioning shorthand like ?>
that was suggested?
On Sun, Jul 12, 2020 at 12:39 AM Johan notifications@github.com wrote:
How about:
const ifNotNull = f => a => a && f(a) collidesHow(entityA, entityB) |> ifNotNull(onCollision)
Not sure what it does to performance but since ifNotNull(onCollision) has to be resolved only once, and not every iteration, I think it couldn't hurt too much...
Also if you use ifNotNull a lot, you could come up with a shorter name so it doesn't add that many extra characters.
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/tc39/proposal-pipeline-operator/issues/159#issuecomment-657186942, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAJKUOFWJNO26D46OBDTRTTR3FSB3ANCNFSM4JN57JOQ .
-- Kevin Lozandier lozandier@gmail.com lozandier@gmail.com
@Jopie64
That wouldn't short-circuit and end the pipeline on null, though, like (I'm assuming) the ?>
operator would. So for longer pipelines, you might need to wrap every step of the pipeline after first possibly null return value
let newScore = person
|> getScoreOrNull
?> double
|> (_ => add(7, _))
|> (_ => boundScore(0, 100, _));
// would become
let newScore = person
|> getScoreOrNull
|> ifNotNull(double)
|> ifNotNull(_ => add(7, _))
|> ifNotNull(_ => boundScore(0, 100, _));
// or nested pipelines
let newScore = person
|> getScoreOrNull
|> ifNotNull(_ => _
|> double
|> (_ => add(7, _))
|> (_ => boundScore(0, 100, _))
);
True, I didn't think of short-circuiting... Still it could be a less ideal but ok workaround I think when the minimal proposal without optional chaining was accepted.
@noppa My proposed semantics would align with the optional chaining operator.
let newScore = person
|> getScoreOrNull
?> double
|> (_ => add(7, _))
|> (_ => boundScore(0, 100, _))
// Would become
let newScore = person
|> getScoreOrNull
|> ifNotNull(_ => _
|> double
|> (_ => add(7, _))
|> (_ => boundScore(0, 100, _))
)
// Or as a statement
let score = getScoreOrNull(person)
let newScore = score != null ? boundScore(0, 100, add(7, double(score))) : undefined
@isiahmeadows Yeah, makes sense.
To clarify, I didn't mean that my "alternative examples" were 100% equivalent to the ?>
version or the result of transpiling pipelines. We were talking about how one might achieve "optional" pipelining without this ?>
proposal if it didn't get in for the first pipeline version, and
getScoreOrNull
|> ifNotNull(..)
|> ifNotNull(..)
is something I could see myself using as a workaround instead of nested pipelines or breaking the pipeline to a statement, at least in some cases.
But it's definitely less than ideal workaround, a short-circuiting ?>
operator would be much nicer to use.
@funkjunky I didn't write a proposal for this, but I would love to include it here: https://xixixao.github.io/proposal-hack-pipelines/index.html#sec-additional-feature-op .
Should null ?> (x => x)
evaluate to undefined
(like null?.()
does) or null
?
I believe it should evaluate to undefined
.
@phaux The idea is that it'd carry the same propagation semantics, so it would return undefined
in this case.
The nuance of language creation is something I'd leave to people more experience than me. Null or undefined... it's not very important to me. Personally I do think returning what the last piped function returned would be more useful to a developer, but I find very few uses in differentiating between null and undefined. Just to be sure, I think we all agree that false should pass through and continue piping.
@noppa You're example is awesome, but I think it'd help to add one more example to show an additional beenfit of the optional operator:
let newScore = person
|> getScoreOrNull
?> double
?> (_ => add(7, _))
?> (_ => boundScore(0, 100, _))
// Would become
let newScore = person
|> getScoreOrNull
|> ifNotNull(_ => _
|> double
|> ifNotNull(_ => _
|> add(7, _)
|> ifNotNull(_ => _
boundScore(0, 100, _)
)
)
)
I may have gotten the syntax wrong, but you get the idea. If you wanted to make the last 3 functions only be piped if non-null, then it becomes very messy.
@funkjunky I didn't write a proposal for this, but I would love to include it here: https://xixixao.github.io/proposal-hack-pipelines/index.html#sec-additional-feature-op .
Are you going to write it or do you want me to write it? I'd like to help in any way that can turn this addition closer to a reality :p [I feel like I'm must continue the war fighting with other devs to show javascript is a great powerful language... unfortunately in my 12 year career I've met far too many, a majority, [bad :p] programmers who disparage Javascript]
If you wanted to make the last 3 functions only be piped if non-null, then it becomes very messy.
No need to nest them:
let newScore = person
|> getScoreOrNull
|> ifNotNull(double)
|> ifNotNull(_ => add(7, _))
|> ifNotNull(_ => boundScore(0, 100, _))
If you wanted to make the last 3 functions only be piped if non-null, then it becomes very messy.
No need to nest them:
let newScore = person |> getScoreOrNull |> ifNotNull(double) |> ifNotNull(_ => add(7, _)) |> ifNotNull(_ => boundScore(0, 100, _))
The un‑nested version won’t have the correct short‑circuiting behaviour.
The idea was to show a benefit, so there should be a better option than unrealistic nested functions to avoid evaluating expressions with no side-effects.
The un‑nested version won’t have the correct short‑circuiting behaviour.
It won't? From what I can tell, it mostly would. The only difference is, it would call ifNotNull two additional times, while the nesting would not. Because the value doesn't change between these calls they're functionally equivilent.
I think my biggest problem with ifNotNull
is the scenario where you use it once, but DON'T nest it.
Because the following (that @noppa wrote) are not logically equivilent:
let newScore = person
|> getScoreOrNull
|> ifNotNull(double)
|> ifNotNull(_ => add(7, _))
|> ifNotNull(_ => boundScore(0, 100, _));
let newScore = person
|> getScoreOrNull
|> ifNotNull(_ => _
|> double
|> (_ => add(7, _))
|> (_ => boundScore(0, 100, _))
);
If getScoreOrNull returns null,then they are equivilent, but if getScoreOrNull returns a non-null value and then double returns null, the first block would return null, until passed through all ifNotNull functions, the second block would call double with null, and continue to call each function if non-null values are returned.
This leaves us with this dangerous piece of code:
let newScore = person
|> getScoreOrNull
|> ifNotNull(double)
|> _ => add(7, _)
|> _ => boundScore(0, 100, _);
One may think we'll stop at ifNotNull(double)
, but we won't.
That's where the ?>
syntax is handy. It WILL stop execution if getScoreOrNull is null.
We need something like ?>
for that extra control.
Just my personal opinion, but I think this usage is getting just a bit too tricky. I worry that if we start adding many cute combinations like this, it will get hard to read JavaScript code. @littledan https://github.com/tc39/proposal-pipeline-operator/issues/159#issuecomment-565922363
IMHO making this combination (optional+pipeline chaining) is an important one and feels more like applying the idea of "optional chaining" evenly across the language. People, being familiar with optional chaining already, once they start using the pipeline chaining, will definitely run into situations where they instinctively want to combine the two.
With the current hack-style proposal, I guess what I'd do is something like
const notNil = val => val == null ? undefined : true
let newScore = person
|> getScoreOrNull(^)
|> (notNil(^) && (double(^)
|> add(7, ^)
|> boundScore(0, 100, ^)
))
Maybe not the ugliest piece of code ever but it's not great either with all the extra parens and stuff.
Would still prefer the ?>
. This use case actually comes up fairly often.
Yeah, I think ?|>
(or whatever similar syntax) has some pretty compelling reasoning behind it. Pipe can be thought of as a function-call operator, and we have optional-function-call via foo.?()
, so it makes sense to me that the same use-cases that drove .?()
would apply here equally.
(I mean, ultimately, this is just baking the Option monad into the language at the syntax level, just as await
is baking the Promise monad in, and it would be sweet to have better support for arbitrary monadic chaining. But we've committed to optional-chaining as a thing, and should probably be consistent in its application.)
With the current hack-style proposal, I guess what I'd do is something like
const notNil = val => val == null ? undefined : true let newScore = person |> getScoreOrNull(^) |> (notNil(^) && (double(^) |> add(7, ^) |> boundScore(0, 100, ^) ))
Maybe not the ugliest piece of code ever but it's not great either with all the extra parens and stuff. Would still prefer the
?>
. This use case actually comes up fairly often.
That code style is hard to look at without wincing…
Yeah it's not great. Personally, I'd probably just break the pipe and use an if()
to execute the rest at that point, rather than nesting things like that.
Why not a fold
to keep the pipeline processing composable & functional?
Could you expand on that?
@tabatkins Sure: You'd avoid breaking up the pipe for if()
by using either
/left
/right
functional semantics by using a fold(funcToFunOnErrorOrFalseyValue, normalFuncToRun)
; it's typically pretty odd to break a pipeline chain just for that when using a functional composition tool such as a pipeline operator:
// Written for the sake of being consistent with how I explained `fold`
const doSomethingWithValidValue = value => value |> double(^) |> add(7, ^) |> boundScore(0. 100, ^)
const newScore = person
|> getScoreOrNull(^)
|> fold(doSomethingWithNullValue, doSomethingWithValidValue)
Edited: Corrected typo of doSomethingWithValidValue
missing value => value
(Assuming doSomethingWithValidValue
is meant to be an arrow function.)
Yeah, you can def abstract it away, but that does mean you can't write your pipe inline; you have to pull the later chunk out into a separate function.
Correct, it was meant to be an arrow function. It's equivalent doing the following (assuming fold is data-last, but it doesn't have to be, just more aligning with the natural composition nature of it all pipeline operator typically helps illustrate):
fold(doSomethingWithNullValue, doSomethingWithValidValue, getScoreOrNull(x));
@tabatkins Am I correct to realize that the following code snippet for the curryable fold
function
|> fold(doSomethingWithNullValue, doSomethingWithValidValue)`
Would have to be the following instead?
|> fold(doSomethingWithNullValue, doSomethingWithValidValue)(^)
If so…. I have to say that immediately adds cognitive load for a operator usually associated with being a functional composition operator that facilities functional chaining first & foremost (which I'm aware needs to be sold as the common case most perceive of it; I think that's very feasible to prove towards a possible syntax change during this stage of the proposal).
Just to be clear, I'm only pointing out fold
in case you weren't aware of it as a functional composition solution to if()
without breaking from a functional composition chain that a functional composition construct/abstraction like pipe()
or the pipeline operator encourages.
You simply wouldn't write a curried fold()
function unless you were explicitly intending to use it in a HOF-oriented library. It's a foreign pattern if you're writing code in any other context, as it requires you to write your function like:
function fold(nullCB, nonNullCB) {
return value=>{
...actual function code here...
}
}
If you're intending to use it non-HOF code, you'd just write
function fold(value, nullCB, nonNullCB) {
...actual code here...
}
// or maybe with "value" as last arg,
// whatever floats your boat
// and then you can write
const x = val |> fold(^, cb1, cb2) |> ...
// or just this, if appropriate
const x = fold(val, cb1, cb2);
If you want to be flexible to use it in both contexts, you're already in a minority of a minority, but it's not too hard to write a transformer function that can convert a function in one of the forms to allowing both forms. Ramda has an auto-currying transformer, I know; I expect most HOF-oriented utility libraries do.
Edit: Fix minor semantic mistake
It's not an uncommon occurrence to want to do something like this:
If support for optional chaining was added, this could be simplified to just this, providing some pretty massive wins in source size (I'm using Elixir-style syntax here, but you could translate it to others easily enough):
Here's what they'd look minified, if you're curious
``` var t=v?.foo?.bar,r=null!=t?f(t,b):void 0 // Original var r=v?.foo?.bar?|>f(b) // With my suggestion here ```