6 ms·
I've felt this same sort of pain but I don't blame currying. It's when currying and partial application is used for deep dependency injection with no easy route
by neurotrace 5y ago
I've felt this same sort of pain but I don't blame currying. It's when currying and partial application is used for deep dependency injection with no easy route to finding the original function. I dabbled in writing an extension for F# that would trace back function calls to give you all of the possible original sources for a value. I think that would solve that particular problem.
Currying and partial application are really nice when you have a pipeline operator, which is why the partial application and pipeline operator proposals are so intertwined.
users
|> List.map (fn user -> user.Email)
|> sendEmail
|> Result.mapError (fn _ -> "Error sending emails")
- okasaki 5y agoI don't see why that's better than the good old for loop. errors = list() for user in users: if not sendEmail(user.email) errors.append("Error sending emails")
- neurotrace 5y agoThe example I gave was contrived. I frequently find myself modeling much of my logic as a series of data transformations. The more I can model logic like this, the easier it is to test. For what it's worth, your example doesn't map 1-to-1 to the intention of what I wrote. Doing it in a more imperative style, I'd have to write emails = list() for user in users: emails.push(user.email) res = sendEmail emails if res.IsError: Error("Error sending emails") else: res (excuse my pseudo Python)
- BiteCode_dev 5y agoExcuses accepted. Imperative Python would really look like: try: return sendEmail(user.email for user in users) except IsError: Error("Error sending emails") # if it does something I agree FP pipelines are nice, but not everything needs it.
- HugoDaniel 5y agonice one, however shouldn't good old for loops be done in ALGOL instead?
- p2t2p 5y agoFor me, the functional/pipeline/call chain style changes perception from "how" to "what". Because I say "what" do to do without going into details of "how" I feel like I have spent less mental capacity reading this portion of code. However, it does require your transformations to be simple and composable, shove a giant multi-line lambda in there it yes, the loop will be better.
- masklinn 5y ago> Currying and partial application are really nice when you have a pipeline operator Currying != partial application. And while currying is useful in curried languages (to convert back from uncurried to curried), and partial application is useful period, the question is whether currying is useful, in general, in an uncurried language. I don't think it is: * You usually want to perform partial application, currying is an unnecessary intermediate step. * Currying conflicts with rich "imperative" parameters e.g. overloading, default parameters, keyword parameters. * Uncurried languages are usually imperative and effectful, that you have filled all the parameter spots does not mean you want to invoke the function. Having a curried language is neat, and in curried languages converting back from uncurried to curried functions is tremendously useful. But it's not so for uncurried languages (which is most of them). > which is why the partial application and pipeline operator proposals are so intertwined. Again, partial application != currying. And pipeline operators can perform partial application (implicitly or explicitely) on their own. For a flagrant example, just see Clojure: (->> users (map :email) (sendEmail) (mapError (constantly "Error sending emails"))
- BiteCode_dev 5y agoAgreed, and I really with partial() was a builtin in Python, instead of having to fetch it from functools. partial() is useful regularly, and much more explicit/flexible than a lambda for this specific use case.
- brundolf 5y agoI'm not sure I fully appreciate the distinction you're trying to make here: "currying != partial application". Can you clarify? In particular I don't know what's meant by "curried" and "uncurried" languages
- djur 5y agoIn Standard ML and Haskell, all functions are curried by default -- they take a single argument, and the syntax for defining multiple-argument functions is just sugar for defining single-argument functions that return single-argument functions. That's reflected in this Haskell type signature for a function adding two integers: add :: Int -> Int -> Int This is a function that takes one integer and returns a function which itself takes another integer and then finally returns an integer. Syntax sugar allows you to define it and call it like a single function. An uncurried function in these languages is a function that takes multiple arguments as a tuple. add :: (Int, Int) -> Int You don't see a lot of functions defined this way in Haskell because there isn't really any advantage to doing so in a lazy, pure language. It's more common in Standard ML from what I've seen, because SML is strict and impure.
- p2t2p 5y agoOh, the lengths people go to avoid OOP. Java: users.stream() .map(User::getEmail) .map(Email::send) .map(res -> "Error sending email: " + res) .collect(toList())
- y4mi 5y agothe only issue with java in this context is that you'll need to create a new class/transfer object between each function call that does things. with elixir (the previous example) you'd likely use simple hashmaps or lists like {email: "mail"} or [:error, "reason for failure"]. That doesn't sound like its amazing, but actually is in practice, because the whole language is written with that in mind (pattern matching to effectively do function/method overloading depending on the value of each argument for example) its very concise while still being very explicit. The same would be possible with java, but you'll need to create a lot of classes/interfaces/enums/boilerplate to facilitate it. i'd still prefer java any time at a dayjob though, because boring and dumb is generally better if you need to write code that's gonna be in use for decades... and likely going to be changed by a lot of people with varying levels of experience.
- qsort 5y ago> the only issue with java in this context is that you'll need to create a new class/transfer object between each function call that does things No, in most cases you don't. The stream interface is mostly designed around simple Lists, Sets and Maps. It also interacts very well with the Collection framework (e.g. myHashmap.entrySet() yielding a set, etc.) which is part of the standard library. You can extend streams with custom collectors, but rarely if ever you need to define intermediate data structures. You do need to define initial and terminal structures, but I'd argue that's good practice regardless.
- y4mi 5y ago> No, in most cases you don't that lets you pass the result from each function to the next without explicitly stating what form they have, yes. You'll still be missing the conciseness/explicitness because the language isn't meant to be used like that and is missing necessary features in order to facilitate it such as pattern matching by the passed in values into function. to make a simple example, you could theoretically write the following pseudo-code def sendEmail({email}) do // send email end def sendEmail({id}) do // get email address sendEmail({email}) end sendEmail({id: 244}) or to make sure your code stops executing if an error occurs (positional return values this time) [:ok, msg] = sendEmail(lkajsdf)
- z3t4 5y agoYou usually want to send the e-mail in order, with a sleep in between to not overload the e-mail server, and stop at any error, so that you can fix the error and then continue from where the error was. An error could be a malformed e-mail address, or a timeout from the e-mail server. What you do not want is to send 10000 e-mail, then have an error like "Error sending emails", then re-run a few time, only to have some people receive 5 e-mail, and some people receive 0 e-mail.
- neurotrace 5y agoI didn't think I had to make this clear but a lot of people seem to be getting stuck on the specifics of my example code. That example is doing nothing but showing a 5,000 foot view of what pipelines can do. Please don't take my dumb example that was written early in the morning as The One True Way of processing data, sending emails, or handling errors. It's a horrible example of that
- z3t4 5y agoSimplified examples always look neat. Any code/syntax will look simple if it doesn't have async, state and error handling.
- neurotrace 5y agoI can't be bothered to type all of it out on my phone but even adding in those pieces, it doesn't change it much. This is F# so you can easily manage the async via an async computation expression, the retry logic can be encapsulated in the `sendEmails` function, and the error handling is reduced to a `Result` which is very easy to work with in a pipeline. Not everything should be done in a pipeline but pipelines make a lot of things a lot nicer
- nestorD 5y agoYes! Like having both pattern matching and tagged union type in a language, it is the synergy of the features that makes them significantly powerful.
- runarberg 5y agoWe were hoping for similar operators in JavaScript (pipeline `|>` and partial application `?`). However TC39 just decided earlier this month that we users of the language are not worthy of such powers. This was off course meet with a lot of resentment from many JavaScript developers, including my self. https://github.com/tc39/proposal-pipeline-operator https://github.com/tc39/proposal-pipeline-operator
- Zababa 5y agoConsidering what happened with the monadic promises, that was to be expected. I hope we'll at least have the immutable records and tuples so that the JS engine can implement them efficiently. This way JS will become an even better compilation target.