1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
|
<!-- toc number-sections -->
This article is part of the *Beginner's Guide to Elvish* series:
- [Your first Elvish commands](first-commands.html)
- [Arguments and outputs](arguments-and-outputs.html)
- [Variables and loops](variables-and-loops.html)
- [Pipelines and IO](pipelines-and-io.html)
- **Value types**
- [Organizing and reusing code](organizing-and-reusing-code.html)
# Maps
We have learned how you can use `curl` to request a URL and `from-json` to
convert JSON-encoded bytes to Elvish values. Combining these two features allows
us to import data from online JSON APIs: for example, let's use the API from
<https://myip.com> to query our IP address and country:
```elvish-transcript Terminal - elvish
~> curl -s https://api.myip.com
{"ip":"10.0.0.31","country":"Elvendom","cc":"EL"}
~> curl -s https://api.myip.com | from-json
▶ [&cc=EL &country=Elvendom &ip=10.0.0.31]
```
The result is surrounded by `[` and `]`, just like lists; but rather than a
list, it's actually a new type of data structure called **maps**.
Lists consist of elements. Maps, on the other hand, consists of *pairs* of
**keys** and **values**. In Elvish, maps are written like `&key=value`, and
putting writing inside `[` and `]` make the overall data structure a map. (The
[general concept of maps](https://en.wikipedia.org/wiki/Associative_array) is
present in many other languages. In this case, our map is actually converted
from a JSON "object", which you can also think of as a map.)
The key-value structure is useful because if there's a key you know, you can
find the corresponding value by **indexing** the map:
```elvish-transcript Terminal - elvish
~> curl -s https://api.myip.com | put (from-json)[country]
▶ Elvendom
```
The `[country]` used for indexing uses the same `[` and `]` for writing lists
and maps, but has a different meaning in this context. You can even use them
together:
```elvish-transcript Terminal - elvish
~> put [&country=Elvendom][country]
▶ Elvendom
```
Here, the first pair of `[]` delimits a map, and the second pair delimits the
index.
To examine the map without having to make the same request every time, we can
save it in a variable:
```elvish-transcript Terminal - elvish
~> curl -s https://api.myip.com | var info = (from-json)
~> put $info[country]
▶ Elvendom
~> put $info[cc]
▶ EL
```
# List indexing
We can also use the indexing syntax to retrieve an element of a list by its
position:
```elvish-transcript Terminal - elvish
~> var triumvirate = [Julius Crassus Pompey]
~> echo $triumvirate[0]' is the most powerful'
Julius is the most powerful
```
The index [starts at zero](https://en.wikipedia.org/wiki/Zero-based_numbering),
like in many other programming languages.
Instead of just one element, lists also allow you to retrieve a **slice** of
elements. There are two variants: *i*..*j* starts from *i* and doesn't include
*j*, while *i*..=*j* includes *j*:
```elvish-transcript Terminal - elvish
~> put $triumvirate[0..2]
▶ [Julius Crassus]
~> put $triumvirate[0..=2]
▶ [Julius Crassus Pompey]
```
As we can see, the result is another list. This is true even if the result is
one or even zero element:
```elvish-transcript Terminal - elvish
~> put $triumvirate[0..1]
▶ [Julius]
~> put $triumvirate[0..0]
▶ []
```
# Nesting data structures
Lists and maps in Elvish can be arbitrarily *nested*. You can have a list of
lists, for example to represent tabular data:
```elvish-transcript Terminal - elvish
~> var table = [[6 10 2] [-2 0 10]]
~> put $table[0][1]
▶ 10
```
You can have a map where each value is another map, for example to represent
information about different entities (in this case, great ancient people)
```elvish-transcript Terminal - elvish
~> var people = [&Julius=[&title=Dictator &country=Rome]
&Alexander=[&title=King &country=Macedon]]
~> put $people[Julius][title]
▶ Dictator
```
You can even use lists and maps as map keys:
```elvish-transcript Terminal - elvish
~> var map-of-complex-keys = [&[&foo=bar]=map &[foo bar]=list]
~> put $map-of-complex-keys[[&foo=bar]]
▶ map
~> put $map-of-complex-keys[[foo bar]]
▶ list
```
(This example can be a bit of a brain teaser because all three meanings of `[`
and `]` are used. Using maps and lists as map keys is not super common, but it's
convenient when you do need it.)
The possibilities are limitless -- as long as the data fits in your computer's
RAM and the nesting relationship in your brain. And it's not just for
theoretical interest; for a real-world example of a list of maps with list
values, see
[the `update-servers-in-parallel.elv` case study](https://xiaq.me/draft.elv.sh/learn/scripting-case-studies.html#update-servers-in-parallel.elv).
(We'll learn more about some features in that example in
[Organizing and reusing code](organizing-and-reusing-code.html)).
# Strings
String is a value type that we have actually been using the whole time. Any text
not using any special punctuation or whitespaces are strings, as are quoted
strings. Unquoted strings are also known as **barewords**.
# Numbers
In fact, even numbers like `1` in Elvish are strings. Let's examine this
example:
```elvish-transcript Terminal - elvish
~> + 1 2
▶ (num 3)
~> + '1' '2'
▶ (num 3)
```
In this example, both `1` and `2` are strings, so `+ 1 2` and `+ '1' '2'` are
equivalent. Numerical commands like `+` accept strings and know how to treat
them as numbers internally.
This is OK for the most part, but there are situations where using strings as
numbers doesn't do what you need:
```elvish-transcript Terminal - elvish
~> put 1 | to-json
"1"
~> put 1 2 12 | order
▶ 1
▶ 12
▶ 2
```
(The [`order`](../ref/builtin.html#order) command reads value inputs, and
outputs them sorted.)
Elvish also supports a number type, and number values can be constructed using
the [`num`](../ref/builtin.html#num) command. You can use them as arguments to
numerical commands too, and they behave as numbers when converting to JSON or
sorting:
```elvish-transcript Terminal - elvish
~> num 3
▶ (num 3)
~> + (num 1) (num 2)
▶ (num 3)
~> num 1 | to-json
1
~> put (num 1) (num 2) (num 12) | order
▶ (num 1)
▶ (num 2)
▶ (num 12)
```
Since commands like `+` accept both `1` and `(num 1)`, we'll call both
"numbers". To distinguish the latter from the former, we usually call them
**typed numbers**.
As you have probably inferred from how the outputs were shown, even though
numerical commands accept both strings and typed numbers as arguments, they
always output typed numbers. This makes the result easier to use in contexts
like converting to JSON or sorting.
We have only worked with integers so far, but Elvish also supports rational
numbers and floating-point numbers:
```elvish-transcript Terminal - elvish
~> + 1/2 1/3
▶ (num 5/6)
~> + 0.2 0.3
▶ (num 0.5)
```
# Booleans
The [Boolean type](https://en.wikipedia.org/wiki/Boolean_data_type) has two
values, *true* and *false*, written in Elvish as two variables `$true` and
`$false`. Unsurprisingly, Elvish commands that tell you if something is true or
false output Boolean values:
```elvish-transcript Terminal - elvish
~> use str
~> str:has-suffix a.png .png
▶ $true
~> str:has-suffix a.png .jpg
▶ $false
```
Elvish also supports
[Boolean algebra](https://en.wikipedia.org/wiki/Boolean_algebra) operations:
```elvish-transcript Terminal - elvish
~> and $true $false
▶ $false
~> or $true $false
▶ $true
~> not $true
▶ $false
```
## Conditionals
Boolean values can be used to decide whether to do something or not, with the
help of the [`if`](../ref/language.html#if) command:
```elvish-transcript Terminal - elvish
~> if $true {
echo "Yes it's true"
}
Yes it's true
~> if $false {
echo "This shouldn't happen"
}
```
The `if` command is a
[**conditional**](https://en.wikipedia.org/wiki/Conditional_(computer_programming))
command, one of the most basic
[**control flows**](https://en.wikipedia.org/wiki/Control_flow). For loops,
which we have seen earlier, are another type of control flow.
Extending our previous example of converting JPG files to AVIF, let's add an
additional condition: we should only perform the conversion when the AVIF file
doesn't exist yet:
```elvish-transcript Terminal - elvish
~> use os
~> use str
~> for jpg [*.jpg] {
var avif = (str:trim-suffix $jpg .jpg).avif
if (not (os:exists $avif)) { # new condition
gm convert $jpg $avif
}
}
```
# Value pipeline redux
Using what we've learned about values in Elvish, let's build a more interesting
value pipeline:
```elvish-transcript Terminal - elvish
~> curl -s https://xkcd.com/info.0.json | var latest = (from-json)[num] # ①
~> range $latest (- $latest 5) | # ②
each {|n| curl -s https://xkcd.com/$n/info.0.json } | # ③
from-json | # ④
each {|info| echo $info[num]': '$info[title] } # ⑤
2905: Supergroup
2904: Physics vs. Magic
2903: Earth/Venus Venn Diagram
2902: Ice Core
2901: Geographic Qualifiers
```
The code above prints the latest 5 webcomics from <https://xkcd.com>, using its
[JSON API](https://xkcd.com/json.html). Let's examine it step by step:
1. We first request <https://xkcd.com/info.0.json>, which fetches the
information for the latest comic. We convert that to an Elvish map, and save
the value of `num` in `$latest`.
2. The [`range`](../ref/builtin.html#range) command outputs a range of numbers.
The range can be increasing or decreasing, but in both cases, it starts at
the first argument, and stops *before* it reaches the second argument.
In the example output, `$latest` happens to be 2905, so `(- $latest 5)` is
2900. You can see the `range` command in action by running it separately:
```elvish-transcript Terminal - elvish
~> range 2905 2900
▶ (num 2905)
▶ (num 2904)
▶ (num 2903)
▶ (num 2902)
▶ (num 2901)
```
3. The [`each`](../ref/builtin.html#each) command runs the code inside `{` and
`}`, assigning `$n` to each input (we will cover the syntax soon in
[Organizing and reusing code](organizing-and-reusing-code.html)). It's
similar to the `for` command we have seen before, except that it uses input
values rather than list elements. The overall effect is the same as:
```elvish-transcript Terminal - elvish
~> curl -s https://xkcd.com/2095/info.0.json
curl -s https://xkcd.com/2094/info.0.json
curl -s https://xkcd.com/2093/info.0.json
curl -s https://xkcd.com/2092/info.0.json
curl -s https://xkcd.com/2091/info.0.json
...
```
(To input multiple lines of commands at the prompt, press
<kbd>Alt-Enter</kbd> instead of <kbd>Enter</kbd>.)
4. The `from-json` command converts the stream of JSON outputs generated by all
the `curl` commands into a stream of Elvish values, in this case maps.
5. This second `each` command retrieves the values corresponding to the `num`
and `title` keys, and print them.
## Limitations of value pipelines
Value pipelines allow you to manipulate data in a natural way similar to
traditional byte pipelines, but it does have an important shortcoming: it is not
available to external commands. We have already seen that external commands
can't produce value outputs; they also can't consume value inputs. As a result,
if you write an Elvish command that produces value outputs, you have to convert
it to bytes before an external command can make use of it, such as with the
`to-json` command.
# Conclusion
Elvish has a rich system of value types. These types allow you to model
real-world problems however you want, and consume and manipulate data sourced
from elsewhere. Elvish's value pipeline mechanism allows you to express these
operations in a fluid way.
Let's now move on to the final part of this series,
[Organizing and reusing code](organizing-and-reusing-code.html).
|