dry-monads 1.3.1 → 1.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +10 -39
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
- data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
- data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
- data/.github/workflows/ci.yml +74 -0
- data/.github/workflows/docsite.yml +34 -0
- data/.github/workflows/sync_configs.yml +34 -0
- data/.rspec +1 -0
- data/.rubocop.yml +89 -0
- data/CHANGELOG.md +21 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/CONTRIBUTING.md +4 -4
- data/Gemfile +4 -1
- data/LICENSE +17 -17
- data/README.md +2 -2
- data/docsite/source/case-equality.html.md +42 -0
- data/docsite/source/do-notation.html.md +207 -0
- data/docsite/source/getting-started.html.md +142 -0
- data/docsite/source/index.html.md +179 -0
- data/docsite/source/list.html.md +87 -0
- data/docsite/source/maybe.html.md +146 -0
- data/docsite/source/pattern-matching.html.md +68 -0
- data/docsite/source/result.html.md +190 -0
- data/docsite/source/task.html.md +126 -0
- data/docsite/source/tracing-failures.html.md +32 -0
- data/docsite/source/try.html.md +76 -0
- data/docsite/source/unit.html.md +36 -0
- data/docsite/source/validated.html.md +88 -0
- data/lib/dry/monads.rb +1 -0
- data/lib/dry/monads/{undefined.rb → constants.rb} +3 -1
- data/lib/dry/monads/do.rb +2 -1
- data/lib/dry/monads/do/all.rb +2 -2
- data/lib/dry/monads/do/mixin.rb +2 -3
- data/lib/dry/monads/maybe.rb +2 -4
- data/lib/dry/monads/result.rb +1 -1
- data/lib/dry/monads/right_biased.rb +52 -7
- data/lib/dry/monads/validated.rb +1 -1
- data/lib/dry/monads/version.rb +1 -1
- metadata +25 -5
- data/.travis.yml +0 -38
@@ -0,0 +1,179 @@
|
|
1
|
+
---
|
2
|
+
title: Introduction
|
3
|
+
description: Common monads for Ruby
|
4
|
+
layout: gem-single
|
5
|
+
type: gem
|
6
|
+
name: dry-monads
|
7
|
+
sections:
|
8
|
+
- getting-started
|
9
|
+
- maybe
|
10
|
+
- result
|
11
|
+
- do-notation
|
12
|
+
- try
|
13
|
+
- list
|
14
|
+
- task
|
15
|
+
- validated
|
16
|
+
- case-equality
|
17
|
+
- tracing-failures
|
18
|
+
- pattern-matching
|
19
|
+
- unit
|
20
|
+
---
|
21
|
+
|
22
|
+
dry-monads is a set of common monads for Ruby. Monads provide an elegant way of handling errors, exceptions and chaining functions so that the code is much more understandable and has all the error handling, without all the `if`s and `else`s. The gem was inspired by the [Kleisli](https://github.com/txus/kleisli) gem.
|
23
|
+
|
24
|
+
What is a monad, anyway? Simply, [a monoid in the category of endofunctors](https://stackoverflow.com/questions/3870088/a-monad-is-just-a-monoid-in-the-category-of-endofunctors-whats-the-proble%E2%85%BF). The term comes from category theory and some believe monads are tough to understand or explain. It's hard to say why people think so because you certainly don't need to know category theory for using them, just like you don't need it for, say, using functions.
|
25
|
+
|
26
|
+
Moreover, the best way to develop intuition about monads is looking at examples rather than learning theories.
|
27
|
+
|
28
|
+
## How to use it?
|
29
|
+
|
30
|
+
Let's say you have code like this
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
user = User.find_by(id: params[:id])
|
34
|
+
|
35
|
+
if user
|
36
|
+
address = user.address
|
37
|
+
end
|
38
|
+
|
39
|
+
if address
|
40
|
+
city = address.city
|
41
|
+
end
|
42
|
+
|
43
|
+
if city
|
44
|
+
state = city.state
|
45
|
+
end
|
46
|
+
|
47
|
+
if state
|
48
|
+
state_name = state.name
|
49
|
+
end
|
50
|
+
|
51
|
+
user_state = state_name || "No state"
|
52
|
+
```
|
53
|
+
|
54
|
+
Writing code in this style is tedious and error-prone. There were created several "cutting-corners" means to work around this issue. The first is ActiveSupport's `.try` which is a plain global monkey patch on `NilClass` and `Object`. Another solution is using the Safe Navigation Operator `&.` introduced in Ruby 2.3 which is a bit better because this is a language feature rather than an opinionated runtime environment pollution. However, some people think these solutions are hacks and the problem reveals a missing abstraction. What kind of abstraction?
|
55
|
+
|
56
|
+
When all objects from the chain of objects are there we could have this instead:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
state_name = User.find_by(id: params[:id]).address.city.state.name
|
60
|
+
user_state = state_name || "No state"
|
61
|
+
```
|
62
|
+
|
63
|
+
By using the `Maybe` monad you can preserve the structure of this code at a cost of introducing a notion of `nil`-able result:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
state_name = Maybe(User.find_by(id: params[:id])).maybe(&:address).maybe(&:city).maybe(&:state).maybe(&:name)
|
67
|
+
user_state = state_name.value_or("No state")
|
68
|
+
```
|
69
|
+
|
70
|
+
`Maybe(...)` wraps the first value and returns a monadic value which either can be a `Some(user)` or `None` if `user` is `nil`. `maybe(&:address)` transforms `Some(user)` to `Some(address)` but leaves `None` intact. To get the final value you can use `value_or` which is a safe way to unwrap a `nil`-able value. In other words, once you've used `Maybe` you _cannot_ hit `nil` with a missing method. This is remarkable because even `&.` doesn't save you from omitting `|| "No state"` at the end of the computation. Basically, that's what they call "Type Safety".
|
71
|
+
|
72
|
+
A more expanded example is based on _composing_ different monadic values. Suppose, we have a user and address, both can be `nil`, and we want to associate the address with the user:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
user = User.find_by(id: params[:user_id])
|
76
|
+
address = Address.find_by(id: params[:address_id])
|
77
|
+
|
78
|
+
if user && address
|
79
|
+
user.update(address_id: address.id)
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
Again, this implies direct work with `nil`-able values which may end up with errors. A monad-way would be using another method, `bind`:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
maybe_user = Maybe(User.find_by(id: params[:user_id]))
|
87
|
+
|
88
|
+
maybe_user.bind do |user|
|
89
|
+
maybe_address = Maybe(Address.find_by(id: params[:address_id]))
|
90
|
+
|
91
|
+
maybe_address.bind do |address|
|
92
|
+
user.update(address_id: address.id)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
One can say this code is opaque compared to the previous example but keep in mind that in _real code_ it often happens to call methods returning `Maybe` values. In this case, it might look like this:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
find_user(params[:user_id]).bind do |user|
|
101
|
+
find_address(params[:address_id]).bind do |address|
|
102
|
+
Some(user.update(address_id: address.id))
|
103
|
+
end
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
Finally, since 1.0, dry-monads has support for [`do` notation](docs::do-notation) which simplifies this code even more, making it almost regular yet `nil`-safe:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
user = yield find_user(params[:user_id])
|
111
|
+
address = yield find_address(params[:address_id])
|
112
|
+
|
113
|
+
Some(user.update(address_id: address.id))
|
114
|
+
```
|
115
|
+
|
116
|
+
Another widely spread monad is `Result` (also known as `Either`) that serves a similar purpose. A notable downside of `Maybe` is plain `None` which carries no information about where this value was produced. `Result` solves exactly this problem by having two constructors for `Success` and `Failure` cases:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
def find_user(user_id)
|
120
|
+
user = User.find_by(id: user_id)
|
121
|
+
|
122
|
+
if user
|
123
|
+
Success(user)
|
124
|
+
else
|
125
|
+
Failure(:user_not_found)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def find_address(address_id)
|
130
|
+
address = Address.find_by(id: address_id)
|
131
|
+
|
132
|
+
if address
|
133
|
+
Success(address)
|
134
|
+
else
|
135
|
+
Failure(:address_not_found)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
You can compose `find_user` and `find_address` with `bind`:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
find_user(params[:user_id]).bind do |user|
|
144
|
+
find_address(params[:address_id]).bind |address|
|
145
|
+
Success(user.update(address_id: address.id))
|
146
|
+
end
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
The inner block can be simplified with `fmap`:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
find_user(params[:user_id]).bind do |user|
|
154
|
+
find_address(params[:address_id]).fmap |address|
|
155
|
+
user.update(address_id: address.id)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
Or, again, the same code with `do`:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
user = yield find_user(params[:user_id])
|
164
|
+
address = yield find_address(params[:address_id])
|
165
|
+
|
166
|
+
Success(user.update(address_id: address.id))
|
167
|
+
```
|
168
|
+
|
169
|
+
The result of this piece of code can be one of `Success(user)`, `Failure(:user_not_found)`, or `Failure(:address_not_found)`. This style of programming called "Railway Oriented Programming" and can check out [dry-transaction](/gems/dry-transaction) and watch a [nice video](https://fsharpforfunandprofit.com/rop/) on the subject. Also, see [dry-matcher](/gems/dry-matcher) for an example of how to use monads for controlling the flow of code with a result.
|
170
|
+
|
171
|
+
## A word of warning
|
172
|
+
|
173
|
+
Before `do` came around here was a warning about over-using monads, turned out with `do` notation code does not differ much from regular Ruby code. Just don't wrap everything with `Maybe`, come up with conventions.
|
174
|
+
|
175
|
+
If you're interested in functional programming in general, consider learning other languages such as Haskell, Scala, OCaml, this will make you a better programmer no matter what programming language you use on a daily basis. And if not earlier then maybe after that dry-monads will become another instrument in your Ruby toolbox :)
|
176
|
+
|
177
|
+
## Credits
|
178
|
+
|
179
|
+
dry-monads is inspired by Josep M. Bach’s [Kleisli](https://github.com/txus/kleisli) gem and its usage by [dry-transaction](/gems/dry-transaction/) and [dry-types](/gems/dry-types/).
|
@@ -0,0 +1,87 @@
|
|
1
|
+
---
|
2
|
+
title: List
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-monads
|
5
|
+
---
|
6
|
+
|
7
|
+
### `bind`
|
8
|
+
|
9
|
+
Lifts a block/proc and runs it against each member of the list. The block must return a value coercible to a list. As in other monads if no block given the first argument will be treated as callable and used instead.
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
require 'dry/monads/list'
|
13
|
+
|
14
|
+
M = Dry::Monads
|
15
|
+
|
16
|
+
M::List[1, 2].bind { |x| [x + 1] } # => List[2, 3]
|
17
|
+
M::List[1, 2].bind(-> x { [x, x + 1] }) # => List[1, 2, 2, 3]
|
18
|
+
|
19
|
+
M::List[1, nil].bind { |x| [x + 1] } # => error
|
20
|
+
```
|
21
|
+
|
22
|
+
### `fmap`
|
23
|
+
|
24
|
+
Maps a block over the list. Acts as `Array#map`. As in other monads, if no block given the first argument will be treated as callable and used instead.
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
require 'dry/monads/list'
|
28
|
+
|
29
|
+
M = Dry::Monads
|
30
|
+
|
31
|
+
M::List[1, 2].fmap { |x| x + 1 } # => List[2, 3]
|
32
|
+
```
|
33
|
+
|
34
|
+
### `value`
|
35
|
+
|
36
|
+
You always can unwrap the result by calling `value`.
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
require 'dry/monads/list'
|
40
|
+
|
41
|
+
M = Dry::Monads
|
42
|
+
|
43
|
+
M::List[1, 2].value # => [1, 2]
|
44
|
+
```
|
45
|
+
|
46
|
+
### Concatenation
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
require 'dry/monads/list'
|
50
|
+
|
51
|
+
M = Dry::Monads
|
52
|
+
|
53
|
+
M::List[1, 2] + M::List[3, 4] # => List[1, 2, 3, 4]
|
54
|
+
```
|
55
|
+
|
56
|
+
### `head` and `tail`
|
57
|
+
|
58
|
+
`head` returns the first element wrapped with a `Maybe`.
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
require 'dry/monads/list'
|
62
|
+
|
63
|
+
M = Dry::Monads
|
64
|
+
|
65
|
+
M::List[1, 2, 3, 4].head # => Some(1)
|
66
|
+
M::List[1, 2, 3, 4].tail # => List[2, 3, 4]
|
67
|
+
```
|
68
|
+
|
69
|
+
### `traverse`
|
70
|
+
|
71
|
+
Traverses the list with a block (or without it). This method "flips" List structure with the given monad (obtained from the type).
|
72
|
+
|
73
|
+
**Note that traversing requires the list to be typed.**
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
require 'dry/monads/list'
|
77
|
+
|
78
|
+
M = Dry::Monads
|
79
|
+
|
80
|
+
M::List[M::Success(1), M::Success(2)].typed(M::Result).traverse # => Success([1, 2])
|
81
|
+
M::List[M::Maybe(1), M::Maybe(nil), M::Maybe(3)].typed(M::Maybe).traverse # => None
|
82
|
+
|
83
|
+
# also, you can use fmap with #traverse
|
84
|
+
|
85
|
+
M::List[1, 2].fmap { |x| M::Success(x) }.typed(M::Result).traverse # => Success([1, 2])
|
86
|
+
M::List[1, nil, 3].fmap { |x| M::Maybe(x) }.typed(M::Maybe).traverse # => None
|
87
|
+
```
|
@@ -0,0 +1,146 @@
|
|
1
|
+
---
|
2
|
+
title: Maybe
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-monads
|
5
|
+
---
|
6
|
+
|
7
|
+
The `Maybe` monad is used when a series of computations could return `nil` at any point.
|
8
|
+
|
9
|
+
### `bind`
|
10
|
+
|
11
|
+
Applies a block to a monadic value. If the value is `Some` then calls the block passing the unwrapped value as an argument. Returns itself if the value is `None`.
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
extend Dry::Monads[:maybe]
|
15
|
+
|
16
|
+
maybe_user = Maybe(user).bind do |u|
|
17
|
+
Maybe(u.address).bind do |a|
|
18
|
+
Maybe(a.street)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# If user with address exists
|
23
|
+
# => Some("Street Address")
|
24
|
+
# If user or address is nil
|
25
|
+
# => None()
|
26
|
+
|
27
|
+
# You also can pass a proc to #bind
|
28
|
+
|
29
|
+
add_two = -> (x) { Maybe(x + 2) }
|
30
|
+
|
31
|
+
Maybe(5).bind(add_two).bind(add_two) # => Some(9)
|
32
|
+
Maybe(nil).bind(add_two).bind(add_two) # => None()
|
33
|
+
|
34
|
+
```
|
35
|
+
|
36
|
+
### `fmap`
|
37
|
+
|
38
|
+
Similar to `bind` but works with blocks/methods that returns unwrapped values (i.e. not `Maybe` instances).
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
extend Dry::Monads[:maybe]
|
42
|
+
|
43
|
+
Maybe(10).fmap { |x| x + 5 }.fmap { |y| y * 2 }
|
44
|
+
# => Some(30)
|
45
|
+
```
|
46
|
+
|
47
|
+
In 1.x `Maybe#fmap` coerces `nil` values returned from blocks to `None`. This behavior will be changed in 2.0. This will be done because implicit coercion violates the functor laws which in order can lead to a surpising (not in a good sense) behavior. If you expect a block to return `nil`, use `Maybe#maybe` added in 1.3.
|
48
|
+
|
49
|
+
### `maybe`
|
50
|
+
|
51
|
+
Almost identical to `Maybe#fmap` but maps `nil` to `None`. This is similar to how the `&.` operator works in Ruby but does wrapping:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
extend Dry::Monads[:maybe]
|
55
|
+
|
56
|
+
Maybe(user).maybe(&:address).maybe(&:street)
|
57
|
+
|
58
|
+
# If user with address exists
|
59
|
+
# => Some("Street Address")
|
60
|
+
# If user or address is nil
|
61
|
+
# => None()
|
62
|
+
```
|
63
|
+
|
64
|
+
### `value!`
|
65
|
+
|
66
|
+
You always can extract the result by calling `value!`. It will raise an error if you call it on `None`. You can use `value_or` for safe unwrapping.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
extend Dry::Monads[:maybe]
|
70
|
+
|
71
|
+
Some(5).fmap(&:succ).value! # => 6
|
72
|
+
|
73
|
+
None().fmap(&:succ).value!
|
74
|
+
# => Dry::Monads::UnwrapError: value! was called on None
|
75
|
+
|
76
|
+
```
|
77
|
+
|
78
|
+
### `value_or`
|
79
|
+
|
80
|
+
Has one argument, unwraps the value in case of `Some` or returns the argument value back in case of `None`. It's a safe and recommended way of extracting values.
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
extend Dry::Monads[:maybe]
|
84
|
+
|
85
|
+
add_two = -> (x) { Maybe(x + 2) }
|
86
|
+
|
87
|
+
Maybe(5).bind(add_two).value_or(0) # => 7
|
88
|
+
Maybe(nil).bind(add_two).value_or(0) # => 0
|
89
|
+
|
90
|
+
Maybe(nil).bind(add_two).value_or { 0 } # => 0
|
91
|
+
```
|
92
|
+
|
93
|
+
### `or`
|
94
|
+
|
95
|
+
The opposite of `bind`.
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
extend Dry::Monads[:maybe]
|
99
|
+
|
100
|
+
add_two = -> (x) { Maybe(x + 2) }
|
101
|
+
|
102
|
+
Maybe(5).bind(add_two).or(Some(0)) # => Some(7)
|
103
|
+
Maybe(nil).bind(add_two).or(Some(0)) # => Some(0)
|
104
|
+
|
105
|
+
Maybe(nil).bind(add_two).or { Some(0) } # => Some(0)
|
106
|
+
```
|
107
|
+
|
108
|
+
### `and`
|
109
|
+
|
110
|
+
Two values can be chained using `.and`:
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
extend Dry::Monads[:maybe]
|
114
|
+
|
115
|
+
Some(5).and(Some(10)) { |x, y| x + y } # => Some(15)
|
116
|
+
Some(5).and(None) { |x, y| x + y } # => None()
|
117
|
+
None().and(Some(10)) { |x, y| x + y } # => None()
|
118
|
+
|
119
|
+
Some(5).and(Some(10)) # => Some([5, 10])
|
120
|
+
Some(5).and(None()) # => None()
|
121
|
+
```
|
122
|
+
|
123
|
+
### `flatten`
|
124
|
+
|
125
|
+
To remove one level of nesting:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
extend Dry::Monads[:maybe]
|
129
|
+
|
130
|
+
Some(Some(10)).flatten # => Some(10)
|
131
|
+
Some(None()).flatten # => None()
|
132
|
+
None().flatten # => None()
|
133
|
+
```
|
134
|
+
|
135
|
+
### `to_result`
|
136
|
+
|
137
|
+
Maybe values can be converted to Result objects:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
extend Dry::Monads[:maybe, :result]
|
141
|
+
|
142
|
+
Some(10).to_result # => Success(10)
|
143
|
+
None().to_result # => Failure()
|
144
|
+
None().to_result(:error) # => Failure(:error)
|
145
|
+
None().to_result { :block_value } # => Failure(:block_value)
|
146
|
+
```
|
@@ -0,0 +1,68 @@
|
|
1
|
+
---
|
2
|
+
title: Pattern matching
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-monads
|
5
|
+
---
|
6
|
+
|
7
|
+
Ruby 2.7 introduces pattern matchings, it is nicely supported by dry-monads 1.3+.
|
8
|
+
|
9
|
+
### Matching Result values
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
# presumably you do it in a class with `include Dry::Monads[:result]`
|
13
|
+
|
14
|
+
case value
|
15
|
+
in Success(Integer => x)
|
16
|
+
# x is bound to an integer
|
17
|
+
in Success[:created, user] # alternatively: Success([:created, user])
|
18
|
+
# user is bound to the second member
|
19
|
+
in Success(Date | Time)
|
20
|
+
# date or time object
|
21
|
+
in Success([1, *])
|
22
|
+
# any array starting with 1
|
23
|
+
in Success(String => s) if s.size < 100
|
24
|
+
# only if `s` is short enough
|
25
|
+
in Success({ counter: Integer })
|
26
|
+
# matches Success(counter: 50)
|
27
|
+
# doesn't match Success(counter: 50, extra: 50)
|
28
|
+
in Success({ user: User, account: Account => user_account, ** })
|
29
|
+
# matches Success(user: User.new(...), account: Account.new(...), else: ...)
|
30
|
+
# user_account is bound to the value of the `:account` key
|
31
|
+
in Success()
|
32
|
+
# corresponds to Success(Unit)
|
33
|
+
in Success(_)
|
34
|
+
# general success
|
35
|
+
in Failure[:user_not_found]
|
36
|
+
# Failure([:user_not_found])
|
37
|
+
in Failure[error_code, *payload]
|
38
|
+
# ...
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
In the sippet above, the patterns will be tried sequentially. If `value` doesn't match any pattern, an error will be thrown.
|
43
|
+
|
44
|
+
### Matching Maybe
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
case value
|
48
|
+
in Some(Integer => x)
|
49
|
+
# x is an integer
|
50
|
+
in Some(Float | String)
|
51
|
+
# ...
|
52
|
+
in None
|
53
|
+
# ...
|
54
|
+
end
|
55
|
+
```
|
56
|
+
|
57
|
+
### Matching List
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
case value
|
61
|
+
in List[Integer]
|
62
|
+
# any list of size 1 with an integer
|
63
|
+
in List[1, 2, 3, *]
|
64
|
+
# list with size >= 3 starting with 1, 2, 3
|
65
|
+
in List[]
|
66
|
+
# empty list
|
67
|
+
end
|
68
|
+
```
|