Piping data to functions
Something I've seen elsewhere is the concept of piping data to functions, which can drastically increase readability in cases where someone would either use intermediate variables or call functions as parameters.
Pseudocode for a contrived example:
x = {1,-7,-5,3}
// Without pipes, intermediate variables.
a = sum(x)
b = abs(a)
c = sqrt(b)
result = round(c, 2)
// Without pipes, nesting functions.
result = round(sqrt(abs(sum(x))), 2)
// With pipes the result on the left of the pipe is passed as the first parameter
//of the function on the right, allowing the code to be read left to right.
result = x %>% sum() %>% abs() %>% sqrt() %>% round(2)
// It's particularly nice for splitting code across lines
result =
x %>%
sum() %>%
abs() %>%
sqrt() %>%
round(2)
The %>% operator is just an example (from R), it could be something else
I'm just learning rust, and know nothing about language development, so the complexities involved might make this impractical. At the same time, it would also be really interesting to see how something like this could be incorporated to work with tuples and multiple return values.
If your functions do take a &self as first parameter, you can achieve similar result with dot chaining:
result = x.sum()
.abs()
.sqrt()
.round(2);
I think you're stuck with the reverse polish dot chaining on &self or self for Rust.
As an aside, if you consider a language like Haskell, Idris, OCaML, etc. where juxtaposition is high-precedence curried function application, then you have both . that acts as function composition and $ that acts as low-precedence curried function application. I'd consider this the most elegant and expressive way of doing this. And I hope someone eventually develops such a language built on borrowing instead of garbage collection. I do think Rust's decision to resemble C and object oriented languages makes Rust more approachable for most programmers. It also seemingly made it easier to focus on zero-cost abstraction, like by simplifying type-inference. To be precise, Idris now strikes a nice enough compromise on overloading to suggest that Rust could looked like a functional language, but those developments happened concurrently with Rust's development, and Idris' type inference used to be incredibly weak, so dealing with this could've been a distraction for Rust.
It's actually pretty simple to implement this with a wrapper type: https://is.gd/42O3Ns
But I agree that it's nice that Rust code is readable for people who've never seen a functional language and I wouldn't want to break this for purity's sake.
While I'm on a little Rust RFC binge I'll just swing by here and say that I really hope to never see syntax like x %>% y in Rust. One of the things (along with many, many others) that first drew me to Rust was that it's actually pretty syntactically light. Nothing like C99 of course, but much easier to read than Haskell or the like.
I think this is an attempt at building convenient function composition. And i would totally support that!
So, translating the pseudocode above into something more Rust-y:
// ... assume various functions are implemented here.
fn main() {
let x = vec![1, -7, -5, 3];
let a = sum(x);
let b = abs(a);
let c = sqrt(b);
let result = round(c, 2);
let result = round(sqrt(abs(sum(x))), 2);
let result = x %>% sum %>% abs %>% sqrt %>% |x| round(x, 2)
}
I'm not completely opposed to adding an infix function composition operator. But I don't think %>% is the right name for it. Pulling the name from Haskell, you could have either .> (which in Haskell is a convenient name for flip (.), or you can use >>> from Control.Arrow, which is more general):
// ... assume various functions are implemented here.
fn main() {
let x = vec![1, -7, -5, 3];
let a = sum(x);
let b = abs(a);
let c = sqrt(b);
let result = round(c, 2);
let result = round(sqrt(abs(sum(x))), 2);
let result = x >>> sum >>> abs >>> sqrt >>> |x| round(x, 2)
}
I was thinking we could define the bitwise-or operator for functions and closures...
On Jul 2, 2017 12:24 PM, "Andrew Brinker" [email protected] wrote:
So, translating the pseudocode above into something more Rust-y:
// ... assume various functions are implemented here. fn main() { let x = vec![1, -7, -5, 3]; let a = sum(x); let b = abs(a); let c = sqrt(b); let result = round(c, 2); let result = round(sqrt(abs(sum(x))), 2); let result = x %>% sum %>% abs %>% sqrt %>% |x| round(x, 2) }
I'm not completely opposed to adding an infix function composition operator. But I don't think %>% is the right name for it. Pulling the name from Haskell, you could have either .> (which in Haskell is a convenient name for flip (.), or you can use >>> from Control.Arrow, which is more general):
// ... assume various functions are implemented here. fn main() { let x = vec![1, -7, -5, 3]; let a = sum(x); let b = abs(a); let c = sqrt(b); let result = round(c, 2); let result = round(sqrt(abs(sum(x))), 2); let result = x >>> sum >>> abs >>> sqrt >>> |x| round(x, 2) }
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/rust-lang/rfcs/issues/2049#issuecomment-312501785, or mute the thread https://github.com/notifications/unsubscribe-auth/AIazwBBuc6tcJxopsGqvAGO1qdHFnWTZks5sJ8RJgaJpZM4OGqNe .
See also the pipeline crate which implements this in a macro.
There is something worth pointing out here, which is the distinction between (.) and (<<<) (and, equivalently, between (.>) and (>>>)). (.) and (.>) are function composition operators, one right-to-left ((.)) and one left-to-right ((.>)). (<<<) and (>>>) are more generic composition operators. In Haskell, they are defined on Category types, where a Category is defined as having an identity "morphism" (id :: Category cat => cat a a, which is like a generic version of the identity function, id :: a -> a), and having morphism composition, (.) :: Category cat => cat b c -> cat a b -> cat a c (yes, this annoyingly overloads the . operator. Like the last operator, this is a generic version of (.) :: (b -> c) -> (a -> b) -> (a -> c)).
(<<<) is equivalent to the Category version of (.), and is defined as (<<<) :: Category cat => cat b c -> cat a b -> cat a c. (>>>) is the same function with the first two arguments flipped (making it left-to-right rather than right-to-left). Its type is (>>>) :: Category cat => cat a b -> cat b c -> cat a c.
I mention this because if we're going to do infix composition, there's the question of whether we'd want to keep it to functions of one argument (like with (.)), or do we want to (can we? not right now, I believe) make it more generic, and akin to the Category typeclass in Haskell?
| Name | Direction | Defined on |
|---|---|---|
. |
right-to-left | functions of one argument (a -> b) |
.> |
left-to-right | functions of one argument (a -> b) |
<<< |
right-to-left | all categories (with functions described above) |
>>> |
left-to-right | all categories (with functions described above) |
@mark-i-m, that particular overloading seems like it would be confusing. I certainly wouldn't expect bitwise-or on functions to work, much less to provide function composition.
It reminds me a bit of C++ overloading >> and << for I/O, and not just left/right bit shifting.
Call me crazy, but one day we wont be limited to shitty ASCII keyboards, could we start using unicode like ∘ for this?
Only like 75% joking.
In reality foo.bar().baz() should be prefered imo.
@nixpulvis, I mean, APL and APL family languages have all gone down this rabbit hole. In the Haskell world, the proliferation of operators (imo) really hurts readability. I think there's a core set that can be good, but I am generally against going completely crazy with introducing new operators.
Haskell's lens library drives me nuts sometimes, because a lot of the docs use the custom operators rather than the equivalent prefix functions.
Yeah, i guess chaining method calls are the primary alternative... Did anybody know if they equally expressive?
They should be, but might require creating a trait.
You cannot beat the curried languages for expressiveness: If you consider the original example round(sqrt(abs(sum(x))), 2) then you would write this in Haskell as (`round` 2) . sqrt . abs . sum $ x, which reads "compose all these functions and apply them to the value x". No maze of parentheses. No bizarre operators like the proposed backwards composition %>% or strange round(2). Just ordinary function composition and a low precedence function application. And the (`round` 2) could be written \x -> round x 2 or flip round 2 too.
As I said before, I think people interested in these question should be asking : How do we bring borrowing, mutability, Rust ABI, etc., to a curried language? Could it be as thin a layer over Rust as say Dogelang is over Python? etc. It's partially that function composition becomes less useful without currying and combinators.
Upping the level off expressivity isnt always a good thing. I think Haskell itself should be a cautionary tale in what happens when you make something have too many expressive forms. Simplicity should be a goal of any practical language. Readability then comes naturally. Rust already struggles here due to it's complex type system, however I personally believe this is warranted due to the static memory guarantees it provides.
Don't forget OCaml, it is as powerful as Haskell but goes through the Module system with sane names instead of doing operator hell as Haskell does. It comes with very few infix operators, one of those is indeed the pipe operator (|>) and the reverse pipe operator (@@, though they would prefer <| but the associativity does not work out). It does not come with . or <<< or >>> or anything else of the sort, things remain readable. In my opinion Rust has taken far too much from Haskell as it is, which contributes to some of what I consider the syntax woe's in Rust and compiling speed hit (compare to OCaml, which remains readable (and even more so once Implicit Module support lands) and compiles to ~C++ runtime speed but with a compile-time often measured in sub-1s).
@burdges
You cannot beat the curried languages for expressiveness
Are you making a theoretical statement or an experiential one? Personally, if chaining can be done via some sort of trait impl, I think that would be the most rustic, but it really depends on the tradeoffs of how much we would lose over composition...
@nixpulvis I think that's the direction Julia takes, here are some syntax literals.
The only case where I've found myself looking for this was in Iterator, Result, or Option contexts which already encourage a self chain. Having to either break the chain or have some of the items out of order due to a specific function not being a method was painful.
What if we added a trait like the one @oli-obk provided and then implemented that for the three types that encourage the chaining style? That would keep the syntax lightweight but also let those cases use their preferred style. Having it be a trait would also let people implement it for their own types that use the same style.
@OvermindDL1 Maybe it's just me, but I find Haskell way more readable than Rust or any other language syntactically in the C language family. I find that Haskell's abundance of white space really helps me read the code as opposed to code being littered with terminal symbols < > & { } ( ) everywhere.
A significant advantage of compositional programming which you get with pointfree style is that you get away with a complete lack of imagination for variable names since you have none to name.
@burdges is spot on - function composition is not very ergonomic when you can't box anything on the heap anytime you like and when you have to think about lifetimes.
find that Haskell's abundance of white space really helps me read the code as opposed to code being littered with terminal symbols < > & { } ( ) everywhere
I think this is mostly a matter of taste, though... Personally, I find well-formatted rust rather pleasing to the eye...
when you can't box anything on the heap anytime you like and when you have to think about lifetimes
I'm not sure I follow... as I understand it, a composition feature would be sugar for something you can already write, so how would there be any difference?
@mark-i-m I guess it's subjective some degree - and there are worse offenders ;)
Sure, it would be sugar, but getting the same expressive power as Haskell might be hard as you might have to distinguish between different types of functions and closures. Some of the expressive you get from partial application and auto-currying, which might need to transparently box the arguments - which you probably wouldn't like in Rust. All I'm saying is it won't be a trivial task to design this well in Rust.
Occasional Rust user here; I landed here after googling for "rust pipe operator". :-) Given Rust's general railway programming style I’m a little surprised it’s not in the language already! A pipe operator provides a useful visual cue that some data is being transformed and enables non-self-methods to be used. Programming in Elixir, Julia, and OCaml/F# has given me a taste for the |> syntax as they all use that syntax. Its simple and straightfoward without the complexity of a full Haskell-style set of operators. There are likely a few functional programmers using Rust wherein a functional map/fold/etc programming style with a pipe operator would be appreciated. It should be pretty easy for anyone with Unix shell philosophy to grok given the syntatic similarity to Unix | pipes.
Also Javascript has |> pipeline operator proposal 😆
fn main() {
let result = vec![1, -7, -5, 3]
|> sum
|> abs
|> sqrt
|> |x| round(x, 2);
let result = round(sqrt(abs(sum(vec![1, -7, -5, 3]))), 2);
}
Rust has a pipe operator, ()., that even works with the functions in the example:
let result = vec![1.0, -7.0, -5.0, 3.0].into_iter
(). sum::<f32>
(). abs
(). sqrt
(). round
();
println!("{}", result);
Try it on play: https://play.rust-lang.org/?gist=e00f6a9e9738ee655b23c62866d78653&version=stable
More seriously, should it just be easier to make extension methods?
@scottmcm That work only on methods of the receiver, not an arbitrary function
I don't know how well this would work, but we could have a.b(c) desugar to b(a, c) if the type of a does not have a b method. This would be kind of like an extension of UFCS...