5 ms·
This is interesting, but I feel that making shell support such a first-class citizen is a mistake. Shell languages basically treat system binaries as functions,
by huntie 9y ago
This is interesting, but I feel that making shell support such a first-class citizen is a mistake. Shell languages basically treat system binaries as functions, which causes you to lose a lot of power with everything being a string. Using system binaries (cat, echo, etc.) isn't really necessary for a modern command language due to the ubiquity of scripting languages and module systems. It's much more powerful to write procedures that work with structured data to perform similar actions.
One of the things that Shells have gotten right is postfix function composition (the | operator). This allows you to very easily compose functions to perform complex operations, and this isn't really present in scripting languages. Haskell let's you do this with the `&` operator, and you can write a procedure for Scheme that performs similarly. I'm not sure how this would work with Python or JS.
I'm hoping that in the next few years we'll see a larger break from shell syntax. The biggest thing I think holding this back is REPLs. Most scripting languages would do fine as a shell replacement with the addition of a stdlib for the purpose, but they need support for navigating the file system. Maybe tab-completion for paths like Bash does would be best, but I'm still thinking about it.
- aaron-lebo 9y ago> I'm not sure how this would work with Python or JS. .method().method()?
- yoavm 9y agoI think the concept of piping is actually more similar to function(function()) than .method().method(), because you're not limited to the scope of your initial object's methods. And the truth is that functionB(functionA()) reads much worse than functionA | functionB , so yeah I agree that this is something that shell scripting languages got right.
- yellowapple 9y agoFor reference, various functional languages do indeed treat something like 'foo |> bar' as equivalent to 'bar(foo)' (I've used this extensively in Elixir, where it's standard practice; I'm pretty sure this is derived from F# and/or Clojure, both of which - IIRC - use the same operator for the same purpose). It's not really perfect parity with what a shell typically offers, though, since a shell's pipeline steps (usually) run in parallel and indefinitely, consuming an input stream and producing an output stream (whether those streams are text, rich objects, audio/video data, or something else entirely). Elixir/Erlang are well-suited to achieving that parity by spinning up processes and using message passing for I/O, but Elixir's pipeline operator doesn't really do that (last I checked).
- michaelmrose 9y agoClojure has threading macros that turn (function(function(function) into (-> function function function) An interesting take on that is a library called swiss arrows that lets you do (-<> (function arg1 <>) (function <> arg2) (function arg1 arg2 <>) Using the <> to indicate where the result of calling one shall be threaded into the next.
- robto 9y agoThat's in the standard library, actually. It's called as->. (as-> value <> (function1 arg1 <>) (function <> arg2) (function arg1 arg2 <>))
- michaelmrose 9y agoFound the old swiss arrows and didn't realize that as-> even existed thank you.
- huntie 9y agoExactly what yoavm said. The pipelining that shell languages do allows for more complex and more arbitrary data transformations. I feel that doing this in Python would be difficult, but I'm not very familiar with it. In scheme you can do something like this: (define (pipe . exprs) (if (null? (cdr exprs)) (car exprs) (apply pipe (cons ((cadr expr) (car expr)) (cddr expr))))) (pipe '(3 4 5) (lambda (x) (map 1+ x))) ;; '(4 5 6)
- andreareina 9y agoWrote something similar in Python, even called it the same! def pipe(init_val, *funcs): """Apply `init_val` to the composition of `funcs` pipe(init, func1, func2, ..., funcn) -> funcn(...(func2(func1(init_val)))) """ return functools.reduce(lambda val, func: func(val), funcs, init_val) Check out clojure's threading macros[1] which extends that to taking forms, where the current value is spliced in as the first or last argument. e.g., with ->> (thread-last): (->> '(3 4 5) (map 1+) (reduce +)) ;; 15 Fits the bill for short functions, and you can still have lambdas for when you really need them. [1] https://clojure.org/guides/threading_macros https://clojure.org/guides/threading_macros
- huntie 9y ago>threading macros Wow that's pretty awesome. I'd been annoyed with needing to use lambdas for this type of thing. I especially like the as-> macro. I need to take the time to explore Scheme macros to see what other useful abstractions they can provide.
- lewisl9029 9y agoThis is object-oriented method chaining. Not the same thing as functional composition (piping is essentially just left-to-right composition), which is what's mentioned in the OP. It's also nowhere near as flexible, because it requires the return value to be responsible for providing a set of APIs for the next method call on every step of the chain, whereas each function in a functional composition can operate on any arbitrary input/output. There's actually a proposal for a pipeline operator in JS that I personally really like: https://github.com/tc39/proposal-pipeline-operator https://github.com/tc39/proposal-pipeline-operator I have no idea if it's getting enough traction to get standardized anytime soon though. In lieu of that, I personally just use Ramda's `pipe` function: http://ramdajs.com/docs/#pipe http://ramdajs.com/docs/#pipe Which accomplishes the same thing, but with a bit more ceremony, and not quite as convenient in a scripting setting.