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

curry helper for functions that return functions #2007

Closed
wants to merge 1 commit into from

Conversation

kedashoe
Copy link
Contributor

@kedashoe kedashoe commented Dec 11, 2016

Quick PoC. Addresses #2006. The idea is to be totally backwards compatible, functions that this is a fit for can be called as they always could, but can now also be invoked all in one go.

Before:

allPass([gt(5), odd]); => Int -> Bool
allPass([gt(5), odd], 4); => Int -> Bool
allPass([gt(5), odd])(4); => false

After

allPass([gt(5), odd]); => Int -> Bool
allPass([gt(5), odd], 4); => false
allPass([gt(5), odd])(4); => false

Some candidates: allPass anyPass bind cond constructN invoker liftN memoize nthArg once tryCatch

"curryX" could also be an internal function, I'm not sure it needs exporting?

@davidchambers
Copy link
Member

👎 from me. I believe the right approach is to fix the arity of every function—including those provided as arguments—rather than concoct ever more intricate 🍛 functions.

I like this type:

allPass :: Array (a -> Boolean) -> a -> Boolean

If there's demand, we could also provide allPass2 and allPass3:

allPass2 :: Array (a -> b -> Boolean) -> a -> b -> Boolean
allPass3 :: Array (a -> b -> c -> Boolean) -> a -> b -> c -> Boolean

We could also support uncurried predicates:

allPass2_ :: Array ((a, b) -> Boolean) -> a -> b -> Boolean
allPass3_ :: Array ((a, b, c) -> Boolean) -> a -> b -> c -> Boolean

It's unlikely I'll get my way on this, but there's always Sanctuary. 😜

@kedashoe
Copy link
Contributor Author

rather than concoct ever more intricate 🍛 functions

I dunno, I think more intricate currying can be ok if it gives the user a better experience.

I wonder about allPass2, allPass3, etc.. this has always felt like a limitation in Hindley-Milner/Haskell. It cannot have felt good, for example, to write unzip7 and then stop. And if we had a generic unzip such that the compiler could type check everything, and then someone came along and said "no no, this is all wrong, we need unzip1-7", would we then get rid of our generic version and use the numbered ones? I have not written enough Haskell to really know, but it feels awkward.

In any event, to the question at hand.. do you feel like this change would give the user a worse experience with ramda?

@davidchambers
Copy link
Member

I agree that numbered functions are not ideal, and indicate a shortcoming of Haskell.

In JavaScript, though, we have no compiler at all, so the burden of ensuring correctness lies entirely on the shoulders of the programmer. This being the case I consider it important to:

  • use simple constructs; and
  • favour constructs which we as people find easy to reason about.

Simple constructs are important because at some point someone will end up reading the source code to understand why undefined is not a function. Also, simple constructs are less likely to produce edge cases.

The tools at our disposal are reasoning and testing. Testing can tell us that something is wrong, but it can't tell us how to fix the problem. We must reason about our bugs. I'm fairly comfortable reasoning about applications of this function in my programs:

lift2 :: Apply f => (a -> b -> c) -> f a -> f b -> f c

With this function, on the other hand, I'm forced to read the source code:

lift :: (*... -> *) -> ([*]... -> [*])

In any event, to the question at hand.. do you feel like this change would give the user a worse experience with ramda?

I like to opt out of much of JavaScript's dynamic features, in order to give myself a fighting chance of understanding my programs with my limited reasoning ability. Surely many people do not share this preference, though. I've no doubt that a significant proportion of Ramda users can reason about complex programs more effectively than I can. Also, I imagine that some Ramda users don't place much value on reasoning, preferring instead to arrive at working programs via trial and error.

@scull7
Copy link

scull7 commented Dec 12, 2016

Once it was explained to me, I think that the current functionality makes the most sense. I am of the same opinion as @davidchambers. I think that attempting to solve this problem within Ramda will only make it less approachable. The only thing that I think needs work is the documentation. Need to mark functions which are not curried in a much clearer way. The curried nature of the Ramda library as a whole makes a non-curried function behavior surprising.

@CrossEye
Copy link
Member

CrossEye commented Dec 14, 2016

I like the idea. I'll have to look more carefully at the implementation. And I won't be able to tonight or tomorrow.

While I like David's suggestion that one should use simple constructs, I also think that Ramda should cater to the sort of code that Javascript developers use. We're trying to make FP concepts more accessible to JS developers, but that doesn't mean that we're abandoning the norms of the language.

I really appreciate the consistency this could add to the library.

@kedashoe
Copy link
Contributor Author

I've updated the PR message, which was lazy and bad 😞

I like simple constructs too! My whole idea here was to make things simpler and more consistent for the user.

Maybe this function would just be internal?

@rjhilgefort
Copy link
Contributor

I just hit this snag when trying to use allPass with useWith. Admittedly, the signature is clear and I should have read it more closely. I think for consistency's sake, this PR should be pushed along. I see that some of the closely related functions like cond and ifElse operate operate simlarly, but the line gets a little fuzzy for me when we consider things like propSatisfies, pathSatisfies.

My use case is hard to work around this limitation (if we stick with trying to use useWith.... this is a contrived example):

const containsAll = useWith(allPass, [map(contains), identity])

containsAll(['A', 'B', 'C'], '_A_B__C__')
// -> function r(e){return 0===arguments.length||n(e)?r:t.apply(this,arguments)}

My workaround for the time being is to just make a new version of allPass and overwrite Ramda's:

const allPass = curry((preds, data) => allPass(preds)(data));

const containsAll = useWith(allPass, [map(contains), identity])

containsAll(['A', 'B', 'C'], '_A_B__C__') // -> true

It seems this PR has lapsed and if I should create a new issue or something to re-spark the discussion, please let me know.

I like @davidchambers's proposed interfaces and would be happy to take a stab at taking this PR over in that spirit.

@CrossEye
Copy link
Member

(Sorry for the slow reply: just back from vacation.)

I agree that this is worth looking at again if you want to have a stab at it, @rjhilgefort.

@wojpawlik
Copy link
Contributor

curryX sounds like an idea similar to uncurry, maybe uncurry could be generalized to handle this case?

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

Successfully merging this pull request may close these issues.

None yet

6 participants