github.com/elves/elvish@v0.15.0/website/learn/effective-elvish.md (about)

     1  <!-- toc -->
     2  
     3  Elvish is not an entirely new language. Its programming techniques have two
     4  primary sources: traditional Unix shells and functional programming languages,
     5  both dating back to many decades ago. However, the way Elvish combines those two
     6  paradigms is unique in many ways, which enables new ways to write code.
     7  
     8  This document is an advanced tutorial focusing on how to write idiomatic Elvish
     9  code, code that is concise and clear, and takes full advantage of Elvish's
    10  features.
    11  
    12  An appropriate adjective for idiomatic Elvish code, like _Pythonic_ for Python
    13  or _Rubyesque_ for Ruby, is **Elven**. In
    14  [Roguelike games](https://en.wikipedia.org/wiki/Roguelike), Elven items are
    15  known to be high-quality, artful and resilient. So is Elven code.
    16  
    17  # Style
    18  
    19  ## Naming
    20  
    21  Use `dash-delimited-words` for names of variables and functions. Underscores are
    22  allowed in variable and function names, but their use should be limited to
    23  environment variables (e.g. `$E:LC_ALL`) and external commands (e.g. `pkg_add`).
    24  
    25  When building a module, use a leading dash to communicate that a variable or
    26  function is subject to change in future and cannot be relied upon, either
    27  because it is an experimental feature or implementation detail.
    28  
    29  Elvish's core libraries follow the naming convention above.
    30  
    31  ## Indentation
    32  
    33  Indent by two spaces.
    34  
    35  ## Code Blocks
    36  
    37  In Elvish, code blocks in control structures are delimited by curly braces. This
    38  is perhaps the most visible difference of Elvish from most other shells like
    39  bash, zsh or fish. The following bash code:
    40  
    41  ```bash
    42  if true; then
    43    echo true
    44  fi
    45  ```
    46  
    47  Is written like this in Elvish:
    48  
    49  ```elvish
    50  if $true {
    51    echo true
    52  }
    53  ```
    54  
    55  If you have used lambdas in Elvish, you will notice that code blocks are
    56  syntactically just parameter-list-less lambdas.
    57  
    58  In Elvish, you cannot put opening braces of code blocks on the next line. This
    59  won't work:
    60  
    61  ```elvish
    62  if $true
    63  { # wrong!
    64    echo true
    65  }
    66  ```
    67  
    68  Instead, you must write:
    69  
    70  ```elvish
    71  if $true {
    72    echo true
    73  }
    74  ```
    75  
    76  This is because in Elvish, control structures like `if` follow the same syntax
    77  as normal commands, hence newlines terminate them. To make the code block part
    78  of the `if` command, it must appear on the same line.
    79  
    80  # Using the Pipeline
    81  
    82  Elvish is equipped with a powerful tool for passing data: the pipeline. Like in
    83  traditional shells, it is an intuitive notation for data processing: data flows
    84  from left to right, undergoing one transformation after another. Unlike in
    85  traditional shells, it is not restricted to unstructured bytes: all Elvish
    86  values, including lists, maps and even closures, can flow in the pipeline. This
    87  section documents how to make the most use of pipelines.
    88  
    89  ## Returning Values with Structured Output
    90  
    91  Unlike functions in most other programming languages, Elvish commands do not
    92  have return values. Instead, they can write to _structured output_, which is
    93  similar to the traditional byte-based stdout, but preserves all internal
    94  structures of aribitrary Elvish values. The most fundamental command that does
    95  this is `put`:
    96  
    97  ```elvish-transcript
    98  ~> put foo
    99  ▶ foo
   100  ~> x = (put foo)
   101  ~> put $x
   102  ▶ foo
   103  ```
   104  
   105  This is hardly impressive - you can output and recover simple strings using good
   106  old byte-based output as well. But let's try this:
   107  
   108  ```elvish-transcript
   109  ~> put "a\nb" [foo bar]
   110  ▶ "a\nb"
   111  ▶ [foo bar]
   112  ~> s li = (put "a\nb" [foo bar])
   113  ~> put $s
   114  ▶ "a\nb"
   115  ~> put $li[0]
   116  ▶ foo
   117  ```
   118  
   119  Here, two things are worth mentioning: the first value we `put` contains a
   120  newline, and the second value is a list. When we capture the output, we get
   121  those exact values back. Passing structured data is difficult with byte-based
   122  output, but trivial with value output.
   123  
   124  Besides `put`, many other builtin commands and commands in builtin modules also
   125  write to structured output, like `str:split`:
   126  
   127  ```elvish-transcript
   128  ~> use str
   129  ~> str:split , foo,bar
   130  ▶ foo
   131  ▶ bar
   132  ~> words = [(str:split , foo,bar)]
   133  ~> put $words
   134  ▶ [foo bar]
   135  ```
   136  
   137  User-defined functions behave in the same way: they "return" values by writing
   138  to structured stdout. Without realizing that "return values" are just outputs in
   139  Elvish, it is easy to think of `put` as **the** command to "return" values and
   140  write code like this:
   141  
   142  ```elvish-transcript
   143  ~> fn split-by-comma [s]{ use str; put (str:split , $s) }
   144  ~> split-by-comma foo,bar
   145  ▶ foo
   146  ▶ bar
   147  ```
   148  
   149  The `split-by-comma` function works, but it can be written more concisely as:
   150  
   151  ```elvish-transcript
   152  ~> fn split-by-comma [s]{ use str; str:split , $s }
   153  ~> split-by-comma foo,bar
   154  ▶ foo
   155  ▶ bar
   156  ```
   157  
   158  In fact, the pattern `put (some-cmd)` is almost always redundant and equivalent
   159  to just `some-command`.
   160  
   161  Similarly, it is seldom necessary to write `echo (some-cmd)`: it is almost
   162  always equivalent to just `some-cmd`. As an exercise, try simplifying the
   163  following function:
   164  
   165  ```elvish
   166  fn git-describe { echo (git describe --tags --always) }
   167  ```
   168  
   169  ## Mixing Bytes and Values
   170  
   171  Each pipe in Elvish comprises two components: one traditional byte pipe that
   172  carries unstructured bytes, and one value pipe that carries Elvish values. You
   173  can write to both, and output capture will capture both:
   174  
   175  ```elvish-transcript
   176  ~> fn f { echo bytes; put value }
   177  ~> f
   178  bytes
   179  ▶ value
   180  ~> outs = [(f)]
   181  ~> put $outs
   182  ▶ [bytes value]
   183  ```
   184  
   185  This also illustrates that the output capture operator `(...)` works with both
   186  byte and value outputs, and it can recover the output sent to `echo`. When byte
   187  output contains multiple lines, each line becomes one value:
   188  
   189  ```elvish-transcript
   190  ~> x = [(echo "lorem\nipsum")]
   191  ~> put $x
   192  ▶ [lorem ipsum]
   193  ```
   194  
   195  Most Elvish builtin functions also work with both byte and value inputs.
   196  Similarly to output capture, they split their byte input by newlines. For
   197  example:
   198  
   199  ```elvish-transcript
   200  ~> use str
   201  ~> put lorem ipsum | each $str:to-upper~
   202  ▶ LOREM
   203  ▶ IPSUM
   204  ~> echo "lorem\nipsum" | each $str:to-upper~
   205  ▶ LOREM
   206  ▶ IPSUM
   207  ```
   208  
   209  This line-oriented processing of byte input is consistent with traditional Unix
   210  tools like `grep`, `sed` and `awk`. In fact, it is easy to write your own `grep`
   211  in Elvish:
   212  
   213  ```elvish-transcript
   214  ~> use re
   215  ~> fn mygrep [p]{ each [line]{ if (re:match $p $line) { echo $line } } }
   216  ~> cat in.txt
   217  abc
   218  123
   219  lorem
   220  456
   221  ~> cat in.txt | mygrep '[0-9]'
   222  123
   223  456
   224  ```
   225  
   226  (Note that it is more concise to write `mygrep ... < in.txt`, but due to
   227  [a bug](https://github.com/elves/elvish/issues/600) this does not work.)
   228  
   229  However, this line-oriented behavior is not always desirable: not all Unix
   230  commands output newline-separated data. When you want to get the output as is,
   231  as a single string, you can use the `slurp` command:
   232  
   233  ```elvish-transcript
   234  ~> echo "a\nb\nc" | slurp
   235  ▶ "a\nb\nc\n"
   236  ```
   237  
   238  One immediate use of `slurp` is to read a whole file into a string:
   239  
   240  ```elvish-transcript
   241  ~> cat hello.go
   242  package main
   243  
   244  import "fmt"
   245  
   246  func main() {
   247              fmt.Println("vim-go")
   248  }
   249  ~> hello-go = (slurp < hello.go)
   250  ~> put $hello-go
   251  ▶ "package main\n\nimport \"fmt\"\n\nfunc main()
   252  {\n\tfmt.Println(\"vim-go\")\n}\n"
   253  ```
   254  
   255  It is also useful, for example, when working with NUL-separated output:
   256  
   257  ```elvish-transcript
   258  ~> touch "a\nb.go"
   259  ~> mkdir d
   260  ~> touch d/f.go
   261  ~> use str
   262  ~> find . -name '*.go' -print0 | str:split "\000" (slurp)
   263  ▶ "./a\nb.go"
   264  ▶ ./d/f.go
   265  ▶ ''
   266  ```
   267  
   268  In the above command, `slurp` turns the input into one string, which is then
   269  used as an argument to `str:split`. The `str:split` command then splits the
   270  whole input by NUL bytes.
   271  
   272  Note that in Elvish, strings can contain NUL bytes; in fact, they can contain
   273  any byte; this makes Elvish suitable for working with binary data. (Also, note
   274  that the `find` command terminates its output with a NUL byte, hence we see a
   275  trailing empty string in the output.)
   276  
   277  One side note: In the first example, we saw that `bytes` appeared before
   278  `value`. This is not guaranteed: byte output and value output are separate, it
   279  is possible to get `value` before `bytes` in more complex cases. Writes to one
   280  component, however, always have their orders preserved, so in `put x; put y`,
   281  `x` will always appear before `y`.
   282  
   283  ## Prefer Pipes Over Parentheses
   284  
   285  If you have experience with Lisp, you will discover that you can write Elvish
   286  code very similar to Lisp. For instance, to split a string containing
   287  comma-separated value, reduplicate each value (using commas as separators), and
   288  rejoin them with semicolons, you can write:
   289  
   290  ```elvish-transcript
   291  ~> csv = a,b,foo,bar
   292  ~> use str
   293  ~> str:join ';' [(each [x]{ put $x,$x } [(str:split , $csv)])]
   294  ▶ 'a,a;b,b;foo,foo;bar,bar'
   295  ```
   296  
   297  This code works, but it is a bit unreadable. In particular, since `str:split`
   298  outputs multiple values but `each` wants a list argument, you have to wrap the
   299  output of `str:split` in a list with `[(str:split ...)]`. Then you have to do
   300  this again in order to pass the output of `each` to `str:join`. You might wonder
   301  why commands like `str:split` and `each` do not simply output a list to make
   302  this easier.
   303  
   304  The answer to that particular question is in the next subsection, but for the
   305  program at hand, there is a much better way to write it:
   306  
   307  ```elvish-transcript
   308  ~> csv = a,b,foo,bar
   309  ~> use str
   310  ~> str:split , $csv | each [x]{ put $x,$x } | str:join ';'
   311  ▶ 'a,a;b,b;foo,foo;bar,bar'
   312  ```
   313  
   314  Besides having fewer pairs of parentheses (and brackets), this program is also
   315  more readable, because the data flows from left to right, and there is no
   316  nesting. You can see that `$csv` is first split by commas, then each value gets
   317  reduplicated, and then finally everything is joined by semicolons. It matches
   318  exactly how you would describe the algorithm in spoken English -- or for that
   319  matter, any spoken language!
   320  
   321  Both versions work, because commands like `each` and `str:join` that work with
   322  multiple inputs can take their inputs in two ways: they can take the inputs as
   323  one list argument, like in the first version; or from the pipeline, like the
   324  second version. Whenever possible, you should prefer the input-from-pipeline
   325  form: it makes for programs that have little nesting, read naturally.
   326  
   327  One exception to the recommendation is when the input is a small set of things
   328  known beforehand. For example:
   329  
   330  ```elvish-transcript
   331  ~> each $str:to-upper~ [lorem ipsum]
   332  ▶ LOREM
   333  ▶ IPSUM
   334  ```
   335  
   336  Here, using the input-from-argument is completely fine: if you want to use the
   337  input-from-input form, you have to supply the input using `put`, which is also
   338  OK but a bit more wordy:
   339  
   340  ```elvish-transcript
   341  ~> put lorem ipsum | each $str:to-upper~
   342  ▶ LOREM
   343  ▶ IPSUM
   344  ```
   345  
   346  However, not all commands support taking input from the pipeline. For example,
   347  if we want to first join some values with space and then split at commas, this
   348  won't work:
   349  
   350  ```elvish-transcript
   351  ~> use str
   352  ~> str:join ' ' [a,b c,d] | str:split ,
   353  Exception: arity mismatch: arguments here must be 2 values, but is 1 value
   354  [tty], line 1: str:join ' ' [a,b c,d] | str:split ,
   355  ```
   356  
   357  This is because the `str:split` command only ever works with one input (one
   358  string to split), and was not implemented to support taking input from pipeline;
   359  hence it always takes 2 arguments and we got an exception.
   360  
   361  It is easy to remedy this situation however. The `all` command passes its input
   362  to its output, and by capturing its output, we can turn the input into an
   363  argument:
   364  
   365  ```elvish-transcript
   366  ~> use str
   367  ~> str:join ' ' [a,b c,d] | str:split , (all)
   368  ▶ a
   369  ▶ 'b c'
   370  ▶ d
   371  ```
   372  
   373  ## Streaming Multiple Outputs
   374  
   375  In the previous subsection, we remarked that commands like `str:split` and
   376  `each` write multiple output values instead of one list. Why?
   377  
   378  This has to do with another advantage of passing data through the pipeline: in a
   379  pipeline, all commands are executed in parallel. A command in a pipeline does
   380  not need to wait for its previous command to finish running before it can start
   381  processing data. Try this in your terminal:
   382  
   383  ```elvish-transcript
   384  ~> each $str:to-upper~ | each [x]{ put $x$x }
   385  (Start typing)
   386  abc
   387  ▶ ABCABC
   388  xyz
   389  ▶ XYZXYZ
   390  (Press ^D)
   391  ```
   392  
   393  You will notice that as soon as you press Enter after typing `abc`, the output
   394  `ABCABC` is shown. As soon as one input is available, it goes through the entire
   395  pipeline, each command doing its work. This gives you immediate feedback, and
   396  makes good use of multi-core CPUs on modern computers. Pipelines are like
   397  assembly lines in the manufacturing industry.
   398  
   399  If instead of passing multiple values, we pass a list through the pipeline: that
   400  means that each command will now be waiting for its previous command to do all
   401  the processing and pack the results in a list before it can start doing
   402  anything. Now, although the commands themselves are run in parallel, they all
   403  need to be waiting for their previous commands to finish before they can start
   404  doing real work.
   405  
   406  This is why commands like `each` and `str:split` produce multiple values instead
   407  of one list. When writing your functions, try to make them produce multiple
   408  values as well: they will cooperate better with builtin commands, and they can
   409  benefit from the efficiency of parallel computations.
   410  
   411  # Working with Multiple Values
   412  
   413  In Elvish, many constructs can evaluate to multiple values. This can be
   414  surprising if you are not familiar with it.
   415  
   416  To start with, output captures evaluate to all the captured values, instead of a
   417  list:
   418  
   419  ```elvish-transcript
   420  ~> use str
   421  ~> str:split , a,b,c
   422  ▶ a
   423  ▶ b
   424  ▶ c
   425  ~> li = (str:split , a,b,c)
   426  Exception: arity mismatch: assignment right-hand-side must be 1 value, but is 3 values
   427  [tty], line 1: li = (str:split , a,b,c)
   428  ```
   429  
   430  The assignment fails with "arity mismatch" because the right hand side evaluates
   431  to 3 values, but you are attempting to assign them to just one variable. If you
   432  want to capture the results into a list, you have to explicitly do so, either by
   433  constructing a list or using rest variables:
   434  
   435  ```elvish-transcript
   436  ~> use str
   437  ~> li = [(str:split , a,b,c)]
   438  ~> put $li
   439  ▶ [a b c]
   440  ~> @li = (str:split , a,b,c) # equivalent and slightly shorter
   441  ```
   442  
   443  ## Assigning Multiple Variables
   444  
   445  # To Be Continued...
   446  
   447  As of writing, Elvish is neither stable nor complete. The builtin libraries
   448  still have missing pieces, the package manager is in its early days, and things
   449  like a type system and macros have been proposed and considered, but not yet
   450  worked on. Deciding best practices for using feature _x_ can be a bit tricky
   451  when that feature _x_ doesn't yet exist!
   452  
   453  The current version of the document is what the lead developer of Elvish (@xiaq)
   454  has collected as best practices for writing Elvish code in early 2018, between
   455  the release of Elvish 0.11 and 0.12. They apply to aspects of the Elvish
   456  language that are relatively complete and stable; but as Elvish evolves, the
   457  document will co-evolve. You are invited to revisit this document once in a
   458  while!