Why Sponsor Oils? | source | all docs for version 0.23.0 | all versions | oilshell.org
YSH has two major units of code: shell-like proc
, and Python-like func
.
This doc compares the two mechanisms, and gives rough guidelines.
Before going into detail, here's a quick reminder that you don't have to use either procs or funcs. YSH is a language that scales both down and up.
You can start with just a list of plain commands:
mkdir -p /tmp/dest
cp --verbose *.txt /tmp/dest
Then copy those into procs as the script gets bigger:
proc build-app {
ninja --verbose
}
proc deploy {
mkdir -p /tmp/dest
cp --verbose *.txt /tmp/dest
}
build-app
deploy
Then add funcs if you need pure computation:
func isTestFile(name) {
return (name => endsWith('._test.py'))
}
if (isTestFile('my_test.py')) {
echo 'yes'
}
This table summarizes the difference between procs and funcs. The rest of the doc will elaborate on these issues.
Proc | Func | |
Design Influence |
Shell-like. |
Python- and JavaScript-like, but pure. |
Shape |
Procs are shaped like Unix processes: with They're a generalization of Bourne shell "functions". |
Funcs are shaped like mathematical functions. |
Architectural Role (Oils is Exterior First) |
Exterior: processes and files. |
Interior: functions and garbage-collected data structures. |
I/O |
Procs may start external processes and pipelines. Can perform I/O anywhere. |
Funcs need an explicit |
Example Definition |
|
|
Example Call |
Procs can be put in pipelines:
|
Or throw away the return value, which is useful for functions that mutate:
|
Naming Convention |
|
|
Syntax Mode of call site |
Command Mode | Expression Mode |
Kinds of Parameters / Arguments |
Examples shown below. |
(both typed) |
Return Value | Integer status 0-255 |
Any type of value, e.g.
|
Relation to Objects | none |
May be bound to objects:
|
Interface Evolution |
Slower: Procs exposed to the outside world may need to evolve in a compatible or "versionless" way. |
Faster: Funcs may be refactored internally. |
Parallelism? |
Procs can be parallel with:
|
Funcs are inherently serial, unless wrapped in a proc. |
More proc features ... |
||
Kinds of Signature |
Open |
- |
Lazy Args |
|
- |
Now that we've compared procs and funcs, let's look more closely at funcs. They're inherently simpler: they have 2 types of args and params, rather than 4.
YSH argument binding is based on Julia, which has all the power of Python, but
without the "evolved warts" (e.g. /
and *
).
In general, with all the bells and whistles, func definitions look like:
# pos args and named args separated with ;
func f(p1, p2, ...rest_pos; n1=42, n2='foo', ...rest_named) {
return (len(rest_pos) + len(rest_named))
}
Func calls look like:
# spread operator ... at call site
var pos_args = [3, 4]
var named_args = {foo: 'bar'}
var x = f(1, 2, ...pos_args; n1=43, ...named_args)
Note that positional args/params and named args/params can be thought of as two "separate worlds".
This table shows simpler, more common cases.
Args / Params | Call Site | Definition |
Positional Args |
|
|
Spread Pos Args |
|
(as above) |
Rest Pos Params |
|
|
... | ||
Named Args |
|
|
Spread Named Args |
|
(as above) |
Rest Named Params |
|
|
Like funcs, procs have 2 kinds of typed args/params: positional and named.
But they may also have string aka word args/params, and a block arg/param.
In general, a proc signature has 4 sections, like this:
proc p (
w1, w2, ...rest_word; # word params
p1, p2, ...rest_pos; # pos params
n1, n2, ...rest_named; # named params
block # block param
) {
echo 'body'
}
In general, a proc call looks like this:
var pos_args = [3, 4]
var named_args = {foo: 'bar'}
p /bin /tmp (1, 2, ...pos_args; n1=43, ...named_args) {
echo 'block'
}
The block can also be passed as an expression after a second semicolon:
p /bin /tmp (1, 2, ...pos_args; n1=43, ...named_args; block)
Some simpler examples:
Args / Params | Call Site | Definition |
Word args |
|
|
Rest Word Params |
|
|
Spread Word Args |
|
(as above) |
... | ||
Typed Pos Arg |
|
|
Typed Named Arg |
|
|
... | ||
Block Argument |
|
|
All Four Kinds |
|
|
Let's recap the common features of procs and funcs.
...
at call site...
at definitionerror
builtin raises exceptionsThe error
builtin is idiomatic in both funcs and procs:
func f(x) {
if (x <= 0) {
error 'Should be positive' (status=99)
}
}
Tip: reserve such errors for exceptional situations. For example, an input string being invalid may not be uncommon, while a disk full I/O error is more exceptional.
(The error
builtin is implemented with C++ exceptions, which are slow in the
error case.)
&myvar
is of type value.Place
Out params are more common in procs, because they don't have a typed return value.
proc p ( ; out) {
call out->setValue(42)
}
var x
p (&x)
echo "x set to $x" # => x set to 42
But they can also be used in funcs:
func f (out) {
call out->setValue(42)
}
var x
call f(&x)
echo "x set to $x" # => x set to 42
Observation: procs can do everything funcs can. But you may want the purity
and familiar syntax of a func
.
Design note: out params are a nicer way of doing what bash does with declare -n
aka nameref
variables. They don't rely on dynamic
scope.
Procs have some features that funcs don't have.
where [x > 10]
A lazy arg list is implemented with shopt --set parse_bracket
, and is syntax
sugar for an unevaluated value.Expr
.
Longhand:
var my_expr = ^[42 === x] # value of type Expr
assert (myexpr)
Shorthand:
assert [42 === x] # equivalent to the above
argv
TODO: Implement new ARGV
semantics.
When a proc signature omits ()
, it's called "open" because the caller can
pass "extra" arguments:
proc my-open {
write 'args are' @ARGV
}
# All valid:
my-open
my-open 1
my-open 1 2
Stricter closed procs:
proc my-closed (x) {
write 'arg is' $x
}
my-closed # runtime error: missing argument
my-closed 1 # valid
my-closed 1 2 # runtime error: too many arguments
An "open" proc is nearly is nearly identical to a shell function:
shfunc() {
write 'args are' @ARGV
}
Values of type Obj
have an ordered set of name-value bindings, as well as a
prototype chain of more Obj
instances ("parents"). They support these
operators:
.
) looks for attributes or methods with a given name.
BoundFunc
, so that the
first self
argument of a method call is the object itself.->
) looks for mutating methods, which have an M/
prefix.
__invoke__
method makes an Object "Proc-like"First, define a proc, with the first typed arg named self
:
proc myInvoke (word_param; self, int_param) {
echo "sum = $[self.x + self.y + int_param]"
}
Make it the __invoke__
method of an Obj
:
var methods = Object(null, {__invoke__: myInvoke})
var invokable_obj = Object(methods, {x: 1, y: 2})
Then invoke it like a proc:
invokable_obj myword (3)
# sum => 6
Let's review the recommended ways to "return" a value:
return (x)
in a func
.
(x + 1)
should
look different than words.value.Place
instance to a proc or func.
&out
.proc
$(myproc)
read
: myproc | read --all; echo $_reply
Obsolete ways of "returning":
declare -n
aka nameref
variables in bash.Some YSH users may tend toward funcs because they're more familiar. But shell composition with procs is very powerful!
They have at least two kinds of composition that funcs don't have.
See #shell-the-good-parts:
YSH is influenced by both shell and Python, so it has both procs and funcs.
Many programmers will gravitate towards funcs because they're familiar, but procs are more powerful and shell-like.
Make your YSH programs by learning to use procs!
procs vs. funcs both have these concerns:
typed_args.Reader
.So the implementation can be thought of as a 2 × 4 matrix, with some code shared. This code is mostly in ysh/func_proc.py.