Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional chaining support for the pipeline operator? #159

Open
dead-claudia opened this issue Nov 15, 2019 · 48 comments
Open

Optional chaining support for the pipeline operator? #159

dead-claudia opened this issue Nov 15, 2019 · 48 comments
Labels
enhancement New feature or request

Comments

@dead-claudia
Copy link
Contributor

dead-claudia commented Nov 15, 2019

Edit: Fix minor semantic mistake

It's not an uncommon occurrence to want to do something like this:

const temp = value?.foo?.bar
const result = temp != null ? doSomethingWithValue(temp, bar) : undefined

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):

const result = value?.foo?.bar?|>doSomethingWithValue(bar)
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
@nicolo-ribaudo
Copy link
Member

I think that this should be a separate proposal, with a different token (e.g. ?>)

@dead-claudia
Copy link
Contributor Author

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.

@ljharb
Copy link
Member

ljharb commented Nov 15, 2019

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?

@mAAdhaTTah
Copy link
Collaborator

mAAdhaTTah commented Nov 15, 2019

@ljharb That would execute doSomething only if # is nullish, which I think is the opposite of what's intended here.

@ljharb
Copy link
Member

ljharb commented Nov 15, 2019

@mAAdhaTTah then # || doSomething(#) or # == null ? # : doSomething(#), either way no additional syntax support would be needed.

@mAAdhaTTah
Copy link
Collaborator

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.

@KristjanTammekivi
Copy link

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.

@xixixao
Copy link

xixixao commented Dec 16, 2019

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;
}

@littledan
Copy link
Member

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.

@dead-claudia
Copy link
Contributor Author

@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
    Interop code

    BuckleScript doesn't come with DOM bindings and no reasonably stable library exists that provides them automatically, so we have to make our own.

     module Storage : sig
     	type t
     	val getItem : t -> string option
     end
     module Window : sig
     	type t
     	val localStorage : t -> Storage.t
     end
     external window : Window.t = "window" [@@bs.val]
    
     module Storage = struct
     	type t
     	external _getItem : t -> string Js.Nullable.t = "getItem" [@@bs.method]
     	let getItem t = Js.Nullable.toOption (_getItem t)
     end
     module Window = struct
     	type t
     	external _localStorage : t -> Storage.t = "localStorage" [@@bs.get]
     	let localStorage = _localStorage
     end
  • 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.

@hax
Copy link
Member

hax commented Jan 17, 2020

@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.

@dead-claudia
Copy link
Contributor Author

@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 #55 - that covers the this-chaining style (which is technically mathematically equivalent).

@hax
Copy link
Member

hax commented Jan 19, 2020

@isiahmeadows Thank you for the link.

Actually now there are 5 different strategies for a@b(...args):

  1. b.call(a, ..args)
  2. b(a, ...args)
  3. b(...args, a)
  4. b(...args)(a) (F# style)
  5. syntax error, require explicit placeholder like 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.

@dead-claudia
Copy link
Contributor Author

@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.

@lozandier
Copy link

@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.

@mikestopcontinues
Copy link

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 ||>.

@littledan
Copy link
Member

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?

@dead-claudia
Copy link
Contributor Author

It can be a follow-on - it already depends on the pipeline operator making it.

@funkjunky
Copy link

funkjunky commented Jul 12, 2020

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.

@funkjunky
Copy link

funkjunky commented Jul 12, 2020

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.

@Jopie64
Copy link

Jopie64 commented Jul 12, 2020

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.

@lozandier
Copy link

lozandier commented Jul 12, 2020 via email

@noppa
Copy link
Contributor

noppa commented Jul 12, 2020

@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, _))
  );

@Jopie64
Copy link

Jopie64 commented Jul 12, 2020

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.

@dead-claudia
Copy link
Contributor Author

@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

@noppa
Copy link
Contributor

noppa commented Jul 14, 2020

@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.

@xixixao
Copy link

xixixao commented Jul 15, 2020

@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 .

@phaux
Copy link

phaux commented Jul 27, 2020

Should null ?> (x => x) evaluate to undefined (like null?.() does) or null?

@ExE-Boss
Copy link

I believe it should evaluate to undefined.

@dead-claudia
Copy link
Contributor Author

@phaux The idea is that it'd carry the same propagation semantics, so it would return undefined in this case.

@funkjunky
Copy link

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
Copy link

@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]

@charmander
Copy link

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, _))

@ExE-Boss
Copy link

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.

@charmander
Copy link

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.

@funkjunky
Copy link

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.

@zenflow
Copy link

zenflow commented Nov 17, 2020

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 #159 (comment)

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.

@noppa
Copy link
Contributor

noppa commented Sep 9, 2021

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.

@tabatkins
Copy link
Collaborator

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.)

@aadamsx
Copy link

aadamsx commented Sep 9, 2021

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…

@tabatkins
Copy link
Collaborator

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.

@lozandier
Copy link

lozandier commented Sep 9, 2021 via email

@tabatkins
Copy link
Collaborator

Could you expand on that?

@lozandier
Copy link

lozandier commented Sep 9, 2021

@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

@tabatkins
Copy link
Collaborator

(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.

@lozandier
Copy link

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));

@lozandier
Copy link

lozandier commented Sep 10, 2021

@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.

@tabatkins
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests