wryun / es-shell

es: a shell with higher-order functions
http://wryun.github.io/es-shell/
Other
307 stars 25 forks source link

Up for discussion: "Pass" syntax for function composition-like behavior #67

Closed jpco closed 3 days ago

jpco commented 9 months ago

This would be new syntax for the shell -- begin throwing tomatoes as desired. People seemed mildly interested in the idea in #53 so here it is.

Note: This shouldn't be merged as-is! At the very least it would need documentation!

To demonstrate the syntax via an example, take the following command:

; result one two three => %count => echo
3

This is essentially equivalent to

; echo <={%count <={result one two three}}
3

The command with the => syntax is desugared into the following

; echo {result one two three => %count => echo}
{%pass {result one two three} {%count $-} {echo $-}}

where %pass is defined as

fn-%pass = $&noreturn @ first rest {
    local (- = <={$first})
        for (cmd = $rest) - = <=$cmd
}

Because of the use of a dynamic binding for -, you can also put $- in the middle of subcommands (and if you like, wrap a subcommand in a thunk to prevent appending the $- at the end)

; echo {result one two three => {echo $- coda}}
{%pass {result one two three} {{echo $- coda} $-}}
; result one two three => {echo $- coda}
one two three coda

though it's really more hygienic to use a lambda:

; echo {result one two three => @ msg {echo $msg coda}}
{%pass {result one two three} {@ msg{echo $msg coda} $-}}
; result one two three => @ msg {echo $msg coda}
one two three coda

I took as much care as I could to make this interact correctly with redirections.

; echo {go-bananas >[2=1] > /dev/null => echo}
{%pass {%dup 2 1 {%create 1 <={%one /dev/null} {go-bananas}}} {echo $-}}

I also made it permissive with other syntax, like assignment here:

; echo {%fsplit '' 'my cool string' => split = => %count => echo}
{%pass {%fsplit '' 'my cool string'} {split=$-} {%count $-} {echo $-}}
; %fsplit '' 'my cool string' => split = => %count => echo
14
; echo $split 
m y   c o o l   s t r i n g

This assignment is odd to read, but can come in handy. I would respect wanting to make at least some of the syntaxes into errors instead. These are all valid:

$&primitives => ~ sethistory
result @ {echo $x} => let (x = y)  # doesn't do what you might think but is valid syntax
result @ {echo $x} => local (x = y)  # does do what you might think
result @ {echo $x} => for (x = 1 2 3 4)  # doesn't do what you might think but is valid syntax

It would be very neat if this could be a proper "function-composition" operator such that this

fn-whatis = %whatis => echo

worked (producing something equivalent to fn-whatis = @ *{echo <={%whatis $*}}), but that would require additional parser magic, and starts to look like a whole different language at that point.

jpco commented 8 months ago

Okay, I have just realized that this is called, in most functional languages, a "pipe operator" (like https://elixirschool.com/en/lessons/basics/pipe_operator). Obviously it shouldn't be called that in a shell, but the concept is the same (and somewhat distinct from function composition operators).

hyphenrf commented 8 months ago

Yeah the pipe operator (aka reverse application) is a popular feature of functional langs like F#, OCaml, Elixir, Elm.. First seen in Isabelle and popularized with F#.

I will be throwing tomatoes not discussing code, apologies! They will be small and sweet though as I'm mostly in agreement :)

There's another approach which more resembles your de-sugared version, called threading, in Clojure (https://clojure.org/guides/threading_macros) I think it's nicer this way for es purposes, because it feels more contained, and doesn't interact too much with the shell dsl syntax -- it's just a builtin function. In es, returns are mostly useful as a language (not interactive shell) feature, so I think threading should be too, if that makes sense. We have builtins that don't have corresponding syntactic forms already, like %flatten and %split.

FWIW composition is just threading/reverse-application with the initial argument missing:

f = g ∘ h
f x = x ▹ h ▹ g

in es, where partial-application isn't automatic and functions are variadic, threading makes more sense.

This raises a question which is relevant in es but not in any of those langs: how should piping work with these variadic functions? should $1 be passed? $*? $*($#*)?

jpco commented 8 months ago

In es, returns are mostly useful as a language (not interactive shell) feature, so I think threading should be too, if that makes sense. We have builtins that don't have corresponding syntactic forms already, like %flatten and %split.

You're most likely right about this (though I personally use return values in an interactive context fairly often -- but I'll admit I'm probably not the average user!).

My main problem with the Clojure threading-type setup is that, without support in the parser, here's what you get:

; fn-%pass = $&noreturn @ v cmds {
    for (cmd = $cmds) $v = <={$cmd $$v}
  }
; local (words = one two three) %pass words %count echo
3

This is all well and good, but as soon as you move beyond single-word expressions, it starts looking like

; local (words = one.two three.four)
    %pass words @ {%split '.' $*} @ {%flatten '-' $*} echo
one-two-three-four

which, with all the lambdas and explicit arguments, is fairly noisy to my eye. It may not be so bad as to justify extra syntax, though.

(If it's relevant, here's the equivalent command using => as implemented here)

; let (words = one.two three.four)
    %split '.' $words => %flatten '-' => echo
one-two-three-four
jpco commented 6 months ago

It would be very neat if this could be a proper "function-composition" operator such that this

fn-whatis = %whatis => echo

Okay turns out this is actually really easy to add, at least in a vaguely-working form, to the existing code in this PR:

/* in parse.y */
assign : caret '=' caret words                 { $$ = $4; }
| caret '=' caret words PASS words      { $$ = mkpass($4, $6); }
# es
; fn foo {result FOO}
; fn-bar = foo => echo
; whatis bar
%pass {foo} {echo $-}
; bar
FOO

Super neat -- proper function composition! Now I'm way out in total brainstorming mode, though. This would only make sense as part of some general "advanced assignment syntax" feature which included other things like fn-blah = foo | bar and other syntactic constructs --- and REALLY I think it would make sense only within a user-definable syntax system.

jpco commented 3 days ago

I have changed my mind and no longer want this in the shell :)