├── tests ├── config.nims ├── mtestutils.nim ├── twithoptions.nim ├── test2.nim ├── tsafeoptions.nim └── toptionsutils.nim ├── .gitignore ├── optionsutils.nimble ├── src ├── safeoptions.nim └── optionsutils.nim └── README.md /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tests/test2 2 | /tests/toptionsutils 3 | /tests/tsafeoptions 4 | -------------------------------------------------------------------------------- /tests/mtestutils.nim: -------------------------------------------------------------------------------- 1 | const nimHasNewOptionsDollar* = (NimMajor, NimMinor, NimPatch) >= (1, 5, 1) 2 | -------------------------------------------------------------------------------- /optionsutils.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "1.3.0" 4 | author = "PMunch" 5 | description = "Utility macros for easier handling of options in Nim" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | 11 | # Dependencies 12 | 13 | requires "nim >= 0.20.0" 14 | -------------------------------------------------------------------------------- /src/safeoptions.nim: -------------------------------------------------------------------------------- 1 | ## This module exists to provide a safe way to use options. Importing this 2 | ## module will give you all the functionality of the `options` and 3 | ## `optionsutils` modules, but will leave out `get` and `unsafeGet` so that 4 | ## only the safe patterns from `optionsutils` will be available for use. 5 | import options, optionsutils 6 | export options except get, unsafeGet 7 | export optionsutils 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | optionsutils 2 | ============ 3 | 4 | This module implements conveniences for dealing with the `Option` type in 5 | Nim. It is based on 6 | [superfuncs maybe library](https://github.com/superfunc/maybe) and 7 | [Toccatas novel boolean approach](www.toccata.io/2017/10/No-Booleans.html) 8 | but also implements features found elsewhere. 9 | 10 | The goal of this library is to make options in Nim easier and safer to work 11 | with by creating good patterns for option handling. It consists of two files: 12 | `optionsutils` which implements all the features, along with `safeoptions` 13 | which exists to provide a safe way to use options. It gives you all the 14 | functionality of the `options` and `optionsutils` modules, but will leave out 15 | `get` and `unsafeGet` so that only the safe patterns from `optionsutils` will 16 | be available for use. 17 | 18 | To see what `optionsutils` offers, see the 19 | [documentation](https://nimble.directory/docs/optionsutils//optionsutils.html). 20 | -------------------------------------------------------------------------------- /tests/twithoptions.nim: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import optionsutils, options 4 | 5 | suite "withSome/withNone": 6 | test "withSome": 7 | proc a(a,b: Option[int], c: Option[int]): Option[int] {.withSome.} = some(a + b + c) 8 | 9 | check a(some(10), none(int), some(10)) == none(int) 10 | check a(some(10), some(100), some(10)) == some(120) 11 | 12 | test "withNone": 13 | proc b(a: Option[int]): Option[int] {.withNone.} = some(10) 14 | 15 | assert b(none(int)) == some(10) 16 | assert b(some(100)) == none(int) 17 | 18 | test "namedConst": 19 | const namedConst = some(10) 20 | proc a(a,b: Option[int], c: Option[int] = namedConst): Option[int] {.withSome.} = some(a + b + c) 21 | check a(some(10), none(int)) == none(int) 22 | check a(some(10), some(100)) == some(120) 23 | 24 | test "undef in withNone": 25 | when(compiles do: 26 | proc b(a: Option[int]): Option[int] {.withNone.} = 27 | echo a # This is not defined here 28 | some(10) 29 | ): 30 | check false 31 | else: 32 | check true 33 | 34 | test "withSome var argument": 35 | proc a(a,b: Option[int], c: var Option[int]): Option[int] {.withSome.} = 36 | result = some(a + b + c.get()) # `c` needs to be manually unpacked as it is a `var` 37 | c = none(int) 38 | 39 | var cIn = some(10) 40 | check a(some(10), none(int), cIn) == none(int) 41 | check cIn == some(10) 42 | 43 | check a(some(10), some(100), cIn) == some(120) 44 | check cIn == none(int) 45 | 46 | test "withNone var argument": 47 | proc b(a: var Option[int]): Option[int]{.withNone.} = 48 | a = some(200) # `a` is available as it was declared as a `var` 49 | some(10) 50 | 51 | var x = some(10) 52 | check b(x) == none(int) 53 | check x == some(10) 54 | x = none(int) 55 | check b(x) == some(10) 56 | check x == some(200) 57 | -------------------------------------------------------------------------------- /tests/test2.nim: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import options, optionsutils 4 | 5 | import strutils 6 | import ./mtestutils 7 | 8 | proc find(haystack: string, needle: char): Option[int] = 9 | for i, c in haystack: 10 | if c == needle: 11 | return some(i) 12 | return none(int) 13 | 14 | 15 | suite "documentation examples": 16 | var echoed: string 17 | proc mockEcho(input: varargs[string, `$`]) = 18 | echoed = input[0] 19 | for i in 1..input.high: 20 | echoed = echoed & input[i] 21 | 22 | test "echo options": 23 | let found = "abc".find('c') 24 | check found.isSome and found.get() == 2 25 | 26 | withSome found: 27 | some value: mockEcho value 28 | none: discard 29 | check echoed == "2" 30 | reset echoed 31 | 32 | found?.mockEcho 33 | check echoed == "2" 34 | reset echoed 35 | 36 | mockEcho either(found, 0) 37 | check echoed == "2" 38 | reset echoed 39 | 40 | test "optCmp": 41 | let compared = some(5).optCmp(`<`, some(10)) 42 | check compared == some(5) 43 | 44 | test "optAnd": 45 | let x = "green" 46 | # This will print out "5" 47 | mockEcho either(optAnd(optCmp(x, `==`, "green"), 5), 3) 48 | check echoed == "5" 49 | reset echoed 50 | # This will print out "3" 51 | mockEcho either(optAnd(optCmp("blue", `==`, "green"), 5), 3) 52 | check echoed == "3" 53 | reset echoed 54 | 55 | test "wrapCall": 56 | let optParseInt = wrapCall: parseInt(x: string): int 57 | mockEcho optParseInt("10") # Prints "some(10)" 58 | when nimHasNewOptionsDollar: 59 | check echoed == "some(10)" 60 | else: 61 | check echoed == "Some(10)" 62 | reset echoed 63 | 64 | mockEcho optParseInt("bob") # Prints "none(int)" 65 | when nimHasNewOptionsDollar: 66 | check echoed == "none(int)" 67 | else: 68 | check echoed == "None[int]" 69 | reset echoed 70 | 71 | mockEcho either(optParseInt("bob"), 10) # Prints 10, like a default value 72 | check echoed == "10" 73 | reset echoed 74 | 75 | withSome optOr(optParseInt("bob"), 10): 76 | some value: 77 | mockEcho 10 # Prints 10, like a default value, but in a safe access pattern 78 | none: 79 | mockEcho "No value" 80 | check echoed == "10" 81 | reset echoed 82 | -------------------------------------------------------------------------------- /tests/tsafeoptions.nim: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import safeoptions 4 | import strutils 5 | import ./mtestutils 6 | 7 | suite "safety": 8 | test "using safe access pattern": 9 | if some("Hello")?.find('l').`==` 2: 10 | check true 11 | else: 12 | check false 13 | let x = some(100) 14 | withSome x: 15 | some y: 16 | check y == 100 17 | none: 18 | check false 19 | 20 | check(either(some("Correct"), "Wrong") == "Correct") 21 | block wrapCallBlock: 22 | let optParseInt = wrapCall: parseInt(x: string): int 23 | check optParseInt("10") == some(10) 24 | check optParseInt("bob") == none(int) 25 | 26 | block wrapExceptionBlock: 27 | let optParseInt = wrapException: parseInt(x: string) 28 | withSome optParseInt("bob"): 29 | some e: check true 30 | none: check false 31 | 32 | withSome optParseInt("10"): 33 | some e: check false 34 | none: check true 35 | 36 | block wrapErrorCodeBlock: 37 | let optParseInt = wrapErrorCode: parseInt(x: string) 38 | withSome optParseInt("10"): 39 | some e: check e == 10 40 | none: check false 41 | 42 | withSome optParseInt("0"): 43 | some e: check false 44 | none: check true 45 | 46 | check toOpt(100) == some(100) 47 | check toOpt(some(100)) == some(100) 48 | 49 | check optAnd(some("hello"), some(100)) == some(100) 50 | check optAnd(some("hello"), none(int)).isNone 51 | check optAnd(some("hello"), 100) == some(100) 52 | check optAnd("hello", none(int)).isNone 53 | 54 | check optOr(some("hello"), some("world")) == some("hello") 55 | check optOr(none(int), some(100)) == some(100) 56 | 57 | check optCmp(some("hello"), `==`, some("world")) == none(string) 58 | check optCmp(some("hello"), `!=`, some("world")) == some("hello") 59 | check optCmp(some("hello"), `!=`, "world") == some("hello") 60 | check optCmp("hello", `!=`, some("world")) == some("hello") 61 | check optCmp("hello", `!=`, "world") == some("hello") 62 | 63 | test "unable to use unsafe pattern": 64 | when(compiles do: 65 | if some("Hello").find('l').isSome: 66 | echo "Found a value!" 67 | ): 68 | check false 69 | else: 70 | check true 71 | 72 | when(compiles do: 73 | if unsafeGet(some("Hello")) == "Hello": 74 | echo "Found a value!" 75 | ): 76 | check false 77 | else: 78 | check true 79 | 80 | when(compiles do: 81 | if get(some("Hello")) == "Hello": 82 | echo "Found a value!" 83 | ): 84 | check false 85 | else: 86 | check true 87 | 88 | suite "original options": 89 | type RefPerson = ref object 90 | name: string 91 | 92 | proc `==`(a, b: RefPerson): bool = 93 | assert(not a.isNil and not b.isNil) 94 | a.name == b.name 95 | 96 | # work around a bug in unittest 97 | let intNone = none(int) 98 | let stringNone = none(string) 99 | 100 | test "example": 101 | proc find(haystack: string, needle: char): Option[int] = 102 | for i, c in haystack: 103 | if c == needle: 104 | return some i 105 | 106 | check(optCmp("abc".find('c'), `==`, 2) == some(2)) 107 | 108 | let result = "team".find('i') 109 | 110 | check result == intNone 111 | check result.isNone 112 | 113 | test "some": 114 | check some(6).isSome 115 | check some("a").isSome 116 | 117 | test "none": 118 | check(none(int).isNone) 119 | check(not none(string).isSome) 120 | 121 | test "equality": 122 | check some("a") == some("a") 123 | check some(7) != some(6) 124 | check some("a") != stringNone 125 | check intNone == intNone 126 | 127 | when compiles(some("a") == some(5)): 128 | check false 129 | when compiles(none(string) == none(int)): 130 | check false 131 | 132 | test "$": 133 | when nimHasNewOptionsDollar: 134 | check($(some("Correct")) == "some(\"Correct\")") 135 | check($(stringNone) == "none(string)") 136 | else: 137 | check($(some("Correct")) == "Some(\"Correct\")") 138 | check($(stringNone) == "None[string]") 139 | 140 | test "map with a void result": 141 | var procRan = 0 142 | some(123).map(proc (v: int) = procRan = v) 143 | check procRan == 123 144 | intNone.map(proc (v: int) = check false) 145 | 146 | test "map": 147 | check(some(123).map(proc (v: int): int = v * 2) == some(246)) 148 | check(intNone.map(proc (v: int): int = v * 2).isNone) 149 | 150 | test "filter": 151 | check(some(123).filter(proc (v: int): bool = v == 123) == some(123)) 152 | check(some(456).filter(proc (v: int): bool = v == 123).isNone) 153 | check(intNone.filter(proc (v: int): bool = check false).isNone) 154 | 155 | test "flatMap": 156 | proc addOneIfNotZero(v: int): Option[int] = 157 | if v != 0: 158 | result = some(v + 1) 159 | else: 160 | result = none(int) 161 | 162 | check(some(1).flatMap(addOneIfNotZero) == some(2)) 163 | check(some(0).flatMap(addOneIfNotZero) == none(int)) 164 | check(some(1).flatMap(addOneIfNotZero).flatMap(addOneIfNotZero) == some(3)) 165 | 166 | proc maybeToString(v: int): Option[string] = 167 | if v != 0: 168 | result = some($v) 169 | else: 170 | result = none(string) 171 | 172 | check(some(1).flatMap(maybeToString) == some("1")) 173 | 174 | proc maybeExclaim(v: string): Option[string] = 175 | if v != "": 176 | result = some v & "!" 177 | else: 178 | result = none(string) 179 | 180 | check(some(1).flatMap(maybeToString).flatMap(maybeExclaim) == some("1!")) 181 | check(some(0).flatMap(maybeToString).flatMap(maybeExclaim) == none(string)) 182 | 183 | test "SomePointer": 184 | var intref: ref int 185 | check(option(intref).isNone) 186 | intref.new 187 | check(option(intref).isSome) 188 | 189 | let tmp = option(intref) 190 | check(sizeof(tmp) == sizeof(ptr int)) 191 | 192 | test "none[T]": 193 | check(none[int]().isNone) 194 | check(none(int) == none[int]()) 195 | 196 | test "$ on typed with .name": 197 | type Named = object 198 | name: string 199 | 200 | let nobody = none(Named) 201 | when nimHasNewOptionsDollar: 202 | check($nobody == "none(Named)") 203 | else: 204 | check($nobody == "None[Named]") 205 | 206 | test "$ on type with name()": 207 | type Person = object 208 | myname: string 209 | 210 | let noperson = none(Person) 211 | when nimHasNewOptionsDollar: 212 | check($noperson == "none(Person)") 213 | else: 214 | check($noperson == "None[Person]") 215 | 216 | test "Ref type with overloaded `==`": 217 | let p = some(RefPerson.new()) 218 | check p.isSome 219 | 220 | -------------------------------------------------------------------------------- /tests/toptionsutils.nim: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import options, optionsutils 4 | import strutils 5 | import macros 6 | import ./mtestutils 7 | 8 | suite "optionsutils": 9 | var echoed: string 10 | proc mockEcho(input: varargs[string, `$`]) = 11 | echoed = input[0] 12 | for i in 1..input.high: 13 | echoed = echoed & input[i] 14 | var evaluated = 0 15 | proc dummySome(): Option[string] = 16 | evaluated += 1 17 | return some("dummy") 18 | proc dummyStr(): string = 19 | evaluated += 1 20 | return "dummy" 21 | 22 | test "existential operator": 23 | when not compiles(some("Hello world")?.find('w').echo): 24 | check false 25 | check (some("Hello world")?.find('w')).unsafeGet == 6 26 | var evaluated = false 27 | if (some("team")?.find('i')).unsafeGet == -1: 28 | evaluated = true 29 | check evaluated == true 30 | evaluated = false 31 | if (none(string)?.find('i')).isSome: 32 | evaluated = true 33 | check evaluated == false 34 | check (some("Hello")?.find('l')).unsafeGet == 2 35 | 36 | some("Hello")?.find('l').mockEcho 37 | check echoed == "2" 38 | reset echoed 39 | 40 | none(string)?.find('l').mockEcho 41 | check echoed.len == 0 42 | reset echoed 43 | 44 | mockEcho none(string)?.find('l') 45 | when nimHasNewOptionsDollar: 46 | check echoed == "none(int)" 47 | else: 48 | check echoed == "None[int]" 49 | reset echoed 50 | 51 | test "existential operator and bool conversion": 52 | if some("Hello")?.find('l').`==` 2: 53 | mockEcho "This prints" 54 | check echoed == "This prints" 55 | reset echoed 56 | 57 | if none(string)?.find('l').`==` 2: 58 | echo "This doesn't" 59 | check echoed != "This doesn't" 60 | reset echoed 61 | 62 | proc isTwo(x: int): bool = x == 2 63 | 64 | if some("hello")?.find('l').isTwo: 65 | mockEcho "This prints" 66 | check echoed == "This prints" 67 | reset echoed 68 | 69 | # Check that regular Option[bool] can't be used in an if 70 | when compiles(if some(true): echo "Bug"): 71 | check false 72 | 73 | test "existential operator with `$`": 74 | let x = some(100) 75 | check (x?.`$`) == some("100") 76 | let y = none(int) 77 | check (y?.`$`) == none(string) 78 | 79 | test "withSome": 80 | let x = some(100) 81 | withSome x: 82 | some y: 83 | check y == 100 84 | none: 85 | check false 86 | 87 | var res = withSome(none(int)) do: 88 | some _: "Hello" 89 | none: "No value" 90 | 91 | check res == "No value" 92 | 93 | res = withSome(some(3)) do: 94 | some count: "Hello".repeat(count) 95 | none: "No value" 96 | 97 | check res == "HelloHelloHello" 98 | 99 | test "withSome with multiple options": 100 | var res = withSome [some(3), some(5)]: 101 | some [firstPos, secondPos]: 102 | "Found 'o' at position: " & $firstPos & " and 'f' at position " & 103 | $secondPos 104 | none: 105 | "Couldn't find either 'o' or 'f'" 106 | 107 | check res == "Found 'o' at position: 3 and 'f' at position 5" 108 | 109 | withSome [some(3), some(5)]: 110 | some [firstPos, secondPos]: 111 | check firstPos == 3 112 | check secondPos == 5 113 | none: 114 | check false 115 | 116 | withSome [some(3), none(string)]: 117 | some [firstPos, secondPos]: 118 | check false 119 | none: 120 | check true 121 | 122 | test "withSome without side effects": 123 | withSome some(100): 124 | some x: mockEcho "Is hundred" 125 | none: mockEcho "No value" 126 | 127 | check echoed == "Is hundred" 128 | reset echoed 129 | 130 | var sideEffects = 0 131 | proc someWithSideEffect(): Option[int] = 132 | sideEffects += 1 133 | some(100) 134 | 135 | withSome([none(int), someWithSideEffect()]): 136 | some [x, _]: mockEcho x 137 | none: mockEcho "No value" 138 | 139 | check echoed == "No value" 140 | reset echoed 141 | check sideEffects == 0 142 | 143 | let y = withSome([some(100), someWithSideEffect(), some(3)]): 144 | some [x, y, z]: (x + y) * z 145 | none: 0 146 | 147 | check y == 600 148 | check sideEffects == 1 149 | 150 | withSome([some(100), some(200)]): 151 | some _: mockEcho "Has value" 152 | none: mockEcho "No value" 153 | 154 | check echoed == "Has value" 155 | reset echoed 156 | 157 | type NonCaseAble = object 158 | val: string 159 | withSome some(NonCaseAble(val: "hello world")): 160 | some x: mockEcho x.val 161 | none: mockEcho "No value" 162 | 163 | check echoed == "hello world" 164 | reset echoed 165 | 166 | test "either": 167 | check(either(some("Correct"), "Wrong") == "Correct") 168 | check(either(none(string), "Correct") == "Correct") 169 | 170 | test "either without side effect": 171 | # Check that dummyStr isn't called when we have an option 172 | check(either(some("Correct"), dummyStr()) == "Correct") 173 | check evaluated == 0 174 | # Check that dummyStr is called when we don't have an option 175 | check(either(none(string), dummyStr()) == "dummy") 176 | check evaluated == 1 177 | reset evaluated 178 | # Check that dummySome is only called once when used as the some value 179 | check(either(dummySome(), "Wrong") == "dummy") 180 | check evaluated == 1 181 | reset evaluated 182 | 183 | test "wrapCall": 184 | let optParseInt = wrapCall: parseInt(x: string): int 185 | check optParseInt("10") == some(10) 186 | check optParseInt("bob") == none(int) 187 | 188 | test "wrapException": 189 | let optParseInt = wrapException: parseInt(x: string) 190 | withSome optParseInt("bob"): 191 | some e: check true 192 | none: check false 193 | 194 | withSome optParseInt("10"): 195 | some e: check false 196 | none: check true 197 | 198 | test "wrapErrorCode": 199 | let optParseInt = wrapErrorCode: parseInt(x: string) 200 | withSome optParseInt("10"): 201 | some e: check e == 10 202 | none: check false 203 | 204 | withSome optParseInt("0"): 205 | some e: check false 206 | none: check true 207 | 208 | test "toOpt": 209 | check toOpt(100) == some(100) 210 | check toOpt(some(100)) == some(100) 211 | 212 | test "optAnd": 213 | check optAnd(some("hello"), some(100)) == some(100) 214 | check optAnd(some("hello"), none(int)).isNone 215 | check optAnd(some("hello"), 100) == some(100) 216 | check optAnd("hello", none(int)).isNone 217 | 218 | test "optAnd without side effects": 219 | check optAnd(some("Correct"), dummyStr()) == some("dummy") 220 | check evaluated == 1 221 | reset evaluated 222 | 223 | check optAnd(none(int), dummyStr()).isNone 224 | check evaluated == 0 225 | reset evaluated 226 | 227 | test "optOr": 228 | check optOr(some("hello"), some("world")) == some("hello") 229 | check optOr(none(int), some(100)) == some(100) 230 | 231 | test "optOr without side effects": 232 | check optOr(some("hello"), some("world"), dummyStr()) == some("hello") 233 | check evaluated == 0 234 | reset evaluated 235 | 236 | check optOr(none(string), dummyStr()) == some("dummy") 237 | check evaluated == 1 238 | reset evaluated 239 | 240 | test "optCmp": 241 | check some("hello").optCmp(`==`, some("world")) == none(string) 242 | check some("hello").optCmp(`!=`, some("world")) == some("hello") 243 | check some("hello").optCmp(`!=`, "world") == some("hello") 244 | check "hello".optCmp(`!=`, some("world")) == some("hello") 245 | check "hello".optCmp(`!=`, "world") == some("hello") 246 | 247 | test "lacking some": 248 | let x = some(100) 249 | withSome x: 250 | none: 251 | check false 252 | 253 | test "lacking none": 254 | let x = some(100) 255 | withSome x: 256 | some y: 257 | check y == 100 258 | 259 | test "then": 260 | check: 261 | some(100) == true.then(100) 262 | none(int) == false.then(100) 263 | some("Hello") == true.then("Hello") 264 | none(string) == false.then("hello") 265 | -------------------------------------------------------------------------------- /src/optionsutils.nim: -------------------------------------------------------------------------------- 1 | ## This module implements conveniences for dealing with the ``Option`` type in 2 | ## Nim. It is based on 3 | ## `superfuncs maybe library`_ and 4 | ## `Toccatas novel boolean approach`_ 5 | ## but also implements features found elsewhere. 6 | ## 7 | ## The goal of this library is to make options in Nim easier and safer to work 8 | ## with by creating good patterns for option handling. 9 | ## 10 | ## 11 | ## Usage 12 | ## ===== 13 | ## 14 | ## Let's start with the example from the ``options`` module: 15 | ## 16 | ## .. code-block:: nim 17 | ## import options 18 | ## 19 | ## proc find(haystack: string, needle: char): Option[int] = 20 | ## for i, c in haystack: 21 | ## if c == needle: 22 | ## return some(i) 23 | ## return none(int) # This line is actually optional, 24 | ## # because the default is empty 25 | ## 26 | ## .. code-block:: nim 27 | ## let found = "abc".find('c') 28 | ## assert found.isSome and found.get() == 2 29 | ## 30 | ## This is probably a familiar pattern, we get an option value, check if it is 31 | ## a "some" value, and then extract the actual value. But this is verbose and 32 | ## error prone. What if we refactor the code and drop the isSome check, now 33 | ## we're in a scenario where ``found.get()`` will throw an exception if we're 34 | ## not careful. This module offers a couple of alternatives: 35 | ## 36 | ## .. code-block:: nim 37 | ## withSome found: 38 | ## some value: echo value 39 | ## none: discard 40 | ## 41 | ## found?.echo 42 | ## 43 | ## echo either(found, 0) 44 | ## 45 | ## The first way, using ``withSome`` offers a safe unpacking pattern. You pass 46 | ## it an option, or a list of options, and give it branches to evaluate when 47 | ## either all of the options are some, or any of them is a none. The benefit of 48 | ## this pattern is that the options values are unpacked into variables 49 | ## automatically in the applicable branch. This means that you can't mess up a 50 | ## refactoring and move something from the some branch into the none branch. 51 | ## And as long as care is taken to stay away from ``options.get`` and 52 | ## ``options.unsafeGet`` this will ensure you can't have exception cases. 53 | ## ``withSome`` can also be used as a pragma to check that all ``Option[T]`` 54 | ## arguments to a procedure are some and unpack them automatically. 55 | ## 56 | ## The second option is the existential operator, or optional chaining 57 | ## operator. This operator can be put where regular dot-chaining would apply 58 | ## and will only continue execution if the left-hand side is a some. In this 59 | ## example ``echo`` will only be called when ``found`` is a some, and won't 60 | ## return anything. However this also works when the right hand side might 61 | ## return something, in this case it will be wrapped in an ``Option[T]`` that 62 | ## will be a none if the left-hand side is a none. 63 | ## 64 | ## And last but not least, a simple ``either`` template. This takes an option 65 | ## and a default value, but where the regular ``options.get`` with an 66 | ## ``otherwise`` argument the default value may be a procedure that returns a 67 | ## value. This procedure will only be called if the value is a none, making 68 | ## sure that no side-effects from the procedure will happen unless it's 69 | ## necessary. 70 | ## 71 | ## This module also includes the convenient ``optCmp`` template allowing you to 72 | ## easily compare the values of two options in an option aware manner. 73 | ## So instead of having to wrap your options in one of the patterns above you 74 | ## can alse use ``optCmp`` to compare options directly: 75 | ## 76 | ## .. code-block:: nim 77 | ## let compared = some(5).optCmp(`<`, some(10)) 78 | ## 79 | ## Note however that this does not return a boolean, it returns the first value 80 | ## of the comparisson. So the above code will return a some option with the 81 | ## value 5. This means you can use them to filter values for example. And of 82 | ## course if either part of the comparisson is a none, then the result will be 83 | ## a none as well. 84 | ## 85 | ## Besides this we also have ``optAnd`` and ``optOr``, these don't work on the 86 | ## value of the option, but rather on the has-ity of the option. So ``optOr`` 87 | ## will return the first some option, or a none option. And ``optAnd`` will 88 | ## return the first none option, or the last option. This can be used to 89 | ## replace boolean expressions. 90 | ## 91 | ## .. code-block:: Nim 92 | ## let x = "green" 93 | ## # This will print out "5" 94 | ## echo either(optAnd(optCmp(x, `==`, "green"), 5), 3) 95 | ## # This will print out "3" 96 | ## echo either(optAnd(optCmp("blue", `==`, "green"), 5), 3) 97 | ## 98 | ## In the first example ``optAnd`` runs it's first expression, ``optCmp`` which 99 | ## returns an option with the value "green", since it has a value ``optAnd`` 100 | ## runs the second expression ``5`` which is automatically converted to 101 | ## ``some(5)``. Since both of these have a value ``optAnd`` returns the last one 102 | ## ``some(5)``, the ``either`` procedure is just an alias for ``get`` with a 103 | ## default value, since it's first argument has a value it returns that value. 104 | ## 105 | ## In the second example ``optAnd`` runs it's first expression, ``optCmp`` which 106 | ## return an option without a value since the comparisson fails. ``optAnd`` then 107 | ## returns an option without a value, and the ``either`` procedure uses the 108 | ## default value of 3. 109 | ## 110 | ## This example is the same as a ``if x == "green": 5 else: 3`` but where x 111 | ## might not be set at all. 112 | ## 113 | ## And last but not least, in case you have a library that doesn't use options 114 | ## there are wrapper procedures that wrap exceptions and error codes in option 115 | ## returns. This is to work well with the logic operations in this module. 116 | ## 117 | ## .. code-block:: nim 118 | ## let optParseInt = wrapCall: parseInt(x: string): int 119 | ## echo optParseInt("10") # Prints "some(10)" 120 | ## echo optParseInt("bob") # Prints "None[int]" 121 | ## echo either(optParseInt("bob"), 10) # Prints 10, like a default value 122 | ## withSome optOr(optParseInt("bob"), 10): 123 | ## some value: 124 | ## echo 10 # Prints 10, like a default value, but in a safe access pattern 125 | ## none: 126 | ## echo "No value" 127 | 128 | import options, macros 129 | 130 | type ExistentialOption[T] = distinct Option[T] 131 | 132 | converter toBool*(option: ExistentialOption[bool]): bool = 133 | Option[bool](option).isSome and Option[bool](option).unsafeGet 134 | 135 | converter toOption*[T](option: ExistentialOption[T]): Option[T] = 136 | Option[T](option) 137 | 138 | proc toExistentialOption*[T](option: Option[T]): ExistentialOption[T] = 139 | ExistentialOption[T](option) 140 | 141 | macro `?.`*(option: untyped, statements: untyped): untyped = 142 | ## Existential operator. Works like regular dot-chaining, but if 143 | ## the left had side is a ``none`` then the right hand side is not evaluated. 144 | ## In the case that ``statements`` return something the return type of this 145 | ## will be ``ExistentialOption[T]`` where ``T`` is the returned type of 146 | ## ``statements`` or if statements return ``Option[T]`` it will be ``T``. If 147 | ## nothing is returned from ``statements`` this returns nothing. The 148 | ## ``ExistentialOption[T]`` auto-converts to an ``Option[T]`` and the only 149 | ## difference between the two is that a ``ExistentialOption[bool]`` will also 150 | ## auto-convert to a ``bool`` to allow it to be used in if statements. 151 | ## 152 | ## .. code-block:: nim 153 | ## echo some("Hello")?.find('l') ## Prints out Some(2) 154 | ## some("Hello")?.find('l').echo # Prints out 2 155 | ## none(string)?.find('l').echo # Doesn't print out anything 156 | ## echo none(string)?.find('l') # Prints out None[int] (return type of find) 157 | ## # These also work in things like ifs as long as operator precedence is 158 | ## # controlled properly: 159 | ## if some("Hello")?.find('l').`==` 2: 160 | ## echo "This prints" 161 | ## proc equalsTwo(x: int): bool = x == 2 162 | ## if some("Hello")?.find('l').equalsTwo: 163 | ## echo "This also prints" 164 | ## if none(string)?.find('l').`==` 2: 165 | ## echo "This doesn't" 166 | let opt = genSym(nskLet) 167 | var 168 | injected = statements 169 | firstBarren = statements 170 | if firstBarren.kind in {nnkCall, nnkDotExpr, nnkCommand}: 171 | # This edits the tree that injected points to 172 | while true: 173 | if firstBarren[0].kind notin {nnkCall, nnkDotExpr, nnkCommand}: 174 | firstBarren[0] = nnkDotExpr.newTree( 175 | newCall(bindSym("unsafeGet"), opt), firstBarren[0]) 176 | break 177 | firstBarren = firstBarren[0] 178 | else: 179 | injected = nnkDotExpr.newTree( 180 | newCall(bindSym("unsafeGet"), opt), firstBarren) 181 | 182 | result = quote do: 183 | (proc (): auto = 184 | let `opt` = `option` 185 | if `opt`.isSome: 186 | when compiles(`injected`) and not compiles(some(`injected`)): 187 | `injected` 188 | else: 189 | return toExistentialOption(toOpt(`injected`)) 190 | )() 191 | 192 | macro withSome*(options: untyped, body: untyped): untyped = 193 | ## Macro to require a set of options to have a value. This macro takes one or 194 | ## more statements that returns an option, and two cases for how to handle 195 | ## the cases that all the options have a value or that at least one of them 196 | ## doesn't. The easiest example looks something like this: 197 | ## 198 | ## .. code-block:: nim 199 | ## withSome "abc".find('b'): 200 | ## some pos: echo "Found 'b' at position: ", pos 201 | ## none: echo "Couldn't find b" 202 | ## 203 | ## In order to minimize the nesting of these withSome blocks you can pass a 204 | ## list of statements that return an option to require and a list of 205 | ## identifiers to the ``some`` case. When doing this the statements will be 206 | ## executed one by one, terminating before all statements are evaluated if one 207 | ## doesn't return a ``some`` option: 208 | ## 209 | ## .. code-block:: nim 210 | ## withSome ["abc".find('o'), "def".find('f')]: 211 | ## some [firstPos, secondPos]: 212 | ## echo "Found 'o' at position: ", firstPos, " and 'f' at position ", 213 | ## secondPos 214 | ## none: echo "Couldn't find either 'o' or 'f'" 215 | ## 216 | ## This will search for an "o" in the string "abc" which will return a 217 | ## ``none`` option and so we will stop, not search for "f" and run 218 | ## the ``none`` case. If there are any of the values we don't care about, but 219 | ## we still require them to exist we can shadow the identifier. All of these 220 | ## would be valid (this is just an example, it is not allowed to have more 221 | ## than one ``some`` case): 222 | ## 223 | ## .. code-block:: nim 224 | ## withSome [oneThing, anotherThing]: 225 | ## some [firstPos, secondPos]: 226 | ## some [_, secondPos]: 227 | ## some _: 228 | ## withSome [oneThing]: 229 | ## some pos: 230 | ## some _: 231 | ## 232 | ## A withSome block can also be used to return values: 233 | ## 234 | ## .. code-block:: nim 235 | ## let x = withSome(["abc".find('b'), "def".find('f')]): 236 | ## some [firstPos, secondPos]: firstPos + secondPos 237 | ## none: -1 238 | ## echo x # Prints out "3" (1 + 2) 239 | var 240 | noneCase: NimNode = nil 241 | someCase: NimNode = nil 242 | idents: NimNode = nil 243 | for optionCase in body: 244 | case optionCase.kind: 245 | of nnkCall: 246 | if $optionCase[0] != "none": 247 | if $optionCase[0] != "some": 248 | error "Only \"none\" and \"some\" are allowed as case labels", 249 | optionCase[0] 250 | else: 251 | error "Only \"none\" is allowed to not have arguments", optionCase[0] 252 | elif noneCase != nil: 253 | error "Only one \"none\" case is allowed, " & 254 | "previously defined \"none\" case at: " & lineInfo(noneCase), 255 | optionCase[0] 256 | else: 257 | noneCase = optionCase[1] 258 | of nnkCommand: 259 | if $optionCase[0] != "some": 260 | if $optionCase[0] != "none": 261 | error "Only \"none\" and \"some\" are allowed as case labels", 262 | optionCase[0] 263 | else: 264 | error "Only \"some\" is allowed to have arguments", optionCase[0] 265 | elif someCase != nil: 266 | error "Only one \"some\" case is allowed, " & 267 | "previously defined \"some\" case at: " & lineInfo(someCase), 268 | optionCase[0] 269 | else: 270 | if optionCase[1].kind != nnkBracket and optionCase[1].kind != nnkIdent: 271 | error "Must have either a list or a single identifier as arguments", 272 | optionCase[1] 273 | else: 274 | if optionCase[1].kind == nnkBracket: 275 | if options.kind != nnkBracket: 276 | error "When only a single option is passed only a single " & 277 | "identifier must be supplied", optionCase[1] 278 | for i in optionCase[1]: 279 | if i.kind != nnkIdent: 280 | error "List must only contain identifiers", i 281 | elif options.kind == nnkBracket: 282 | if $optionCase[1] != "_": 283 | error "When multiple options are passed all identifiers must be " & 284 | "supplied", optionCase[1] 285 | idents = if optionCase[1].kind == nnkBracket: optionCase[1] else: newStmtList(optionCase[1]) 286 | someCase = optionCase[2] 287 | else: 288 | error "Unrecognized structure of cases", optionCase 289 | if noneCase == nil and someCase == nil: 290 | error "Must have either a \"some\" case, a \"none\" case, or both" 291 | var 292 | body = if someCase != nil: someCase else: nnkDiscardStmt.newTree(newNilLit()) 293 | none = if noneCase != nil: noneCase else: nnkDiscardStmt.newTree(newNilLit()) 294 | let 295 | optionsList = (if options.kind == nnkBracket: options else: newStmtList(options)) 296 | ug = bindSym"unsafeGet" 297 | for i in countdown(optionsList.len - 1, 0): 298 | let 299 | option = optionsList[i] 300 | tmpLet = genSym(nskLet) 301 | ident = if idents.len <= i: newLit("_") else: idents[i] 302 | assign = if $ident != "_": 303 | quote do: 304 | let `ident` = `ug`(`tmpLet`) 305 | else: 306 | newStmtList() 307 | body = quote do: 308 | let `tmpLet` = `option` 309 | if `tmpLet`.isSome: 310 | `assign` 311 | `body` 312 | else: 313 | `none` 314 | result = body 315 | # This doesn't work if `body` includes any reference to result.. 316 | # It was probably done this way for a reason though 317 | #result = quote do: 318 | # (proc (): auto = 319 | # `body` 320 | # )() 321 | 322 | template either*(self, otherwise: untyped): untyped = 323 | ## Similar in function to ``get``, but if ``otherwise`` is a procedure it will 324 | ## not be evaluated if ``self`` is a ``some``. This means that ``otherwise`` 325 | ## can have side effects. 326 | let opt = self # In case self is a procedure call returning an option 327 | if opt.isSome: unsafeGet(opt) else: otherwise 328 | 329 | macro wrapCall*(statement: untyped): untyped = 330 | ## Macro that wraps a procedure which can throw an exception into one that 331 | ## returns an option. This version takes a procedure with arguments and a 332 | ## return type. It returns a lambda that has the same signature as the 333 | ## procedure but returns an Option of the return type. The body executes the 334 | ## statement and returns the value if there is no exception, otherwise it 335 | ## returns a none option. 336 | ## 337 | ## .. code-block:: nim 338 | ## let optParseInt = wrapCall: parseInt(x: string): int 339 | ## echo optParseInt("10") # Prints "some(10)" 340 | ## echo optParseInt("bob") # Prints "none(int)" 341 | assert(statement.kind == nnkStmtList) 342 | assert(statement[0].kind == nnkCall) 343 | assert(statement[0].len == 2) 344 | assert(statement[0][0].kind == nnkObjConstr) 345 | assert(statement[0][0].len >= 1) 346 | assert(statement[0][0][0].kind == nnkIdent) 347 | for i in 1 ..< statement[0][0].len: 348 | assert(statement[0][0][i].kind == nnkExprColonExpr) 349 | assert(statement[0][0][i].len == 2) 350 | assert(statement[0][0][i][0].kind == nnkIdent) 351 | assert(statement[0][1].kind == nnkStmtList) 352 | let T = statement[0][1][0] 353 | let 354 | procName = statement[0][0][0] 355 | result = quote do: 356 | (proc (): Option[`T`] = 357 | try: 358 | return some(`procName`()) 359 | except: 360 | return none[`T`]() 361 | ) 362 | # Add the arguments to the argument list of the proc and the call 363 | for i in 1 ..< statement[0][0].len: 364 | result[0][3].add nnkIdentDefs.newTree(statement[0][0][i][0], statement[0][0][i][1], newEmptyNode()) 365 | result[0][6][0][0][0][0][1].add statement[0][0][i][0] 366 | 367 | macro wrapException*(statement: untyped): untyped = 368 | ## Macro that wraps a procedure which can throw an exception into one that 369 | ## returns an option. This version takes a procedure with arguments but no 370 | ## return type. It returns a lambda that has the same signature as the 371 | ## procedure but returns an ``Option[ref Exception]``. The body executes the 372 | ## statement and returns a none option if there is no exception. Otherwise it 373 | ## returns a some option with the exception. 374 | ## 375 | ## .. code-block:: nim 376 | ## # This might be a silly example, it's more useful for things that 377 | ## # doesn't return anything 378 | ## let optParseInt = wrapException: parseInt(x: string) 379 | ## withSome optParseInt("bob"): 380 | ## some e: echo e.msg # Prints the exception message 381 | ## none: echo "Execution succeded" 382 | assert(statement.len == 1) 383 | assert(statement[0].kind == nnkObjConstr) 384 | assert(statement[0].len >= 1) 385 | assert(statement[0][0].kind == nnkIdent) 386 | for i in 1 ..< statement[0].len: 387 | assert(statement[0][i].kind == nnkExprColonExpr) 388 | assert(statement[0][i].len == 2) 389 | assert(statement[0][i][0].kind == nnkIdent) 390 | let 391 | procName = statement[0][0] 392 | result = quote do: 393 | (proc (): Option[ref Exception] = 394 | try: 395 | discard `procName`() 396 | return none(ref Exception) 397 | except: 398 | return some(getCurrentException()) 399 | ) 400 | # Add the arguments to the argument list of the proc and the call 401 | for i in 1 ..< statement[0].len: 402 | result[0][3].add nnkIdentDefs.newTree(statement[0][i][0], statement[0][i][1], newEmptyNode()) 403 | result[0][6][0][0][0][0].add statement[0][i][0] 404 | 405 | macro wrapErrorCode*(statement: untyped): untyped = 406 | ## Macro that wraps a procedure which returns an error code into one that 407 | ## returns an option. This version takes a procedure with arguments but no 408 | ## return type. It returns a lambda that has the same signature as the 409 | ## procedure but returns an ``Option[int]``. The body executes the 410 | ## statement and returns a none option if the error code is 0. Otherwise it 411 | ## returns a some option with the error code. 412 | ## 413 | ## .. code-block:: nim 414 | ## # We cheat a bit here and use parseInt to emulate an error code 415 | ## let optParseInt = wrapErrorCode: parseInt(x: string) 416 | ## withSome optParseInt("10"): 417 | ## some e: echo "Got error code: ", e 418 | ## none: echo "Execution succeded" 419 | assert(statement.len == 1) 420 | assert(statement[0].kind == nnkObjConstr) 421 | assert(statement[0].len >= 1) 422 | assert(statement[0][0].kind == nnkIdent) 423 | for i in 1 ..< statement[0].len: 424 | assert(statement[0][i].kind == nnkExprColonExpr) 425 | assert(statement[0][i].len == 2) 426 | assert(statement[0][i][0].kind == nnkIdent) 427 | let 428 | procName = statement[0][0] 429 | result = quote do: 430 | (proc (): Option[int] = 431 | let eCode = `procName`() 432 | if eCode == 0: 433 | return none(int) 434 | else: 435 | return some(eCode) 436 | ) 437 | # Add the arguments to the argument list of the proc and the call 438 | for i in 1 ..< statement[0].len: 439 | result[0][3].add nnkIdentDefs.newTree(statement[0][i][0], statement[0][i][1], newEmptyNode()) 440 | result[0][6][0][0][2].add statement[0][i][0] 441 | 442 | proc toOpt*[T](value: Option[T]): Option[T] = 443 | ## Procedure with overload to automatically convert something to an option if 444 | ## it's not already an option. 445 | value 446 | 447 | proc toOpt*[T](value: T): Option[T] = 448 | ## Procedure with overload to automatically convert something to an option if 449 | ## it's not already an option. 450 | some(value) 451 | 452 | macro optAnd*(options: varargs[untyped]): untyped = 453 | ## Goes through all options until one of them is not a some. If one of the 454 | ## options is not a some it returns a none, otherwise it returns the last 455 | ## option. Note that if some of the options are a procedure that returns an 456 | ## Option they won't get evaluated if an earlier option is a none. If any of 457 | ## the options is not an option but another type they will be converted to an 458 | ## option of that type automatically. 459 | var 460 | body = newStmtList() 461 | lastOpt: NimNode 462 | for option in options: 463 | lastOpt = genSym(nskLet) 464 | body.add quote do: 465 | let `lastOpt` = toOpt(`option`) 466 | if not `lastOpt`.isSome: return 467 | body.add quote do: 468 | return `lastOpt` 469 | 470 | result = quote do: 471 | (proc (): auto = `body`)() 472 | 473 | macro optOr*(options: varargs[untyped]): untyped = 474 | ## Goes through the options until one of them is a some. If none of the 475 | ## options are a some a none is returned. Note that if some of the options are 476 | ## a procedure that returns an Option they won't get evaluated if an earlier 477 | ## option is a some. If any of the options is not an option but another type 478 | ## they will be converted to an option of that type automatically. 479 | var body = newStmtList() 480 | for option in options: 481 | body.add quote do: 482 | let opt = toOpt(`option`) 483 | if opt.isSome: return opt 484 | 485 | result = quote do: 486 | (proc (): auto = `body`)() 487 | 488 | template optCmp*(self, cmp, value: untyped): untyped = 489 | ## Comparator for options. ``cmp`` must be something that accepts two 490 | ## parameters, ``self`` and ``value`` can either be ``Option[T]`` or ``T``. 491 | ## Will return ``self`` if it is an ``Option[T]`` or ``self`` converted to 492 | ## an ``Option[T]`` if both ``self`` and ``value`` is a some and ``cmp`` 493 | ## returns true when called with their values. 494 | (proc (): auto = 495 | let 496 | a = toOpt(self) 497 | b = toOpt(value) 498 | if a.isSome and b.isSome: 499 | if `cmp`(unsafeGet(a), unsafeGet(b)): 500 | return a 501 | )() 502 | 503 | macro withSome*(procDef: untyped): untyped = 504 | ## Pragma that can be applied to procedures which takes ``Option[T]`` 505 | ## arguments. It will verify that all the options are some and shadow the 506 | ## argument names with the internal type of the ``Option[T]`` (as long as the 507 | ## argument isn't declared as a ``var``). If one of the arguments isn't 508 | ## ``some`` then the procedure will return before it's body, and the return 509 | ## value will be left as its default (so an ``Option[T]`` return type would be 510 | ## a ``none``). 511 | runnableExamples: 512 | import options 513 | proc addNums(a, b: Option[int], c: var Option[int]): Option[int] {.withSome.} = 514 | assert typeof(a) is int # `a` and `b` are shadowed with the options internal type 515 | assert typeof(b) is int 516 | assert typeof(c) is Option[int] # `c` is not shadowed as it is a `var` 517 | result = some(a + b + c.get()) 518 | c = none(int) 519 | # Since the default of an `Option[int]` is a `none`, if the check of `a` 520 | # and `b` fails this returns `none` 521 | doAssert procDef.kind in {nnkProcDef, nnkFuncDef}, "This macro only works on procedure and function definitions." 522 | let 523 | identDefs = procDef[3] #All parameter names 524 | stmtList = procDef.body #body 525 | for def in identDefs: 526 | let 527 | opt = def.findChild(it.kind in {nnkBracketExpr, nnkVarTy}) 528 | bracket = if opt == nil or opt.kind == nnkBracketExpr: opt else: opt[0] 529 | if bracket != nil and $bracket[0] == "Option": 530 | var foundNonIdent = false #Used to remove redundant constant checks 531 | for varNode in def: 532 | foundNonIdent = varNode.kind != nnkIdent #First N idents are the declared variables in a `a,b,c: T` 533 | if foundNonIdent: break #Hit non ident which means we dont need to iterate any further 534 | stmtList.insert 0, quote do: 535 | if `varNode`.isNone: return 536 | if opt.kind != nnkVarTy: 537 | stmtList.insert 1, quote do: 538 | let `varNode` = `varNode`.unsafeGet 539 | procDef 540 | 541 | macro withNone*(procDef: untyped): untyped = 542 | ## Same as the ``withSome`` pragma but verifies that all options are ``none``. 543 | ## All option arguments are shadowed by an error template so they can't be 544 | ## used within the procedure body, this does not apply to arguments declared 545 | ## as ``var``. 546 | doAssert procDef.kind in {nnkProcDef, nnkFuncDef}, "This macro only works on procedure and function definitions." 547 | let 548 | identDefs = procDef[3] #All parameter names 549 | stmtList = procDef.body 550 | for def in identDefs: 551 | let 552 | opt = def.findChild(it.kind in {nnkBracketExpr, nnkVarTy}) 553 | bracket = if opt == nil or opt.kind == nnkBracketExpr: opt else: opt[0] 554 | #Get all defined variables here so we can check them later 555 | if bracket != nil and $bracket[0] == "Option": 556 | var foundNonIdent = false #Used to remove redundant constant checks 557 | for varNode in def: 558 | foundNonIdent = varNode.kind != nnkIdent #First N idents are the declared variables in a `a,b,c: T` 559 | if foundNonIdent: break #Hit non ident which means we dont need to iterate any further 560 | stmtList.insert 0, quote do: 561 | if `varNode`.isSome: return 562 | if opt.kind != nnkVarTy: 563 | stmtList.insert 1, quote do: 564 | template `varNode`(): untyped = {.error: "Cannot use `" & astToStr(`varNode`) & "` in this `withNone` context".} 565 | procDef 566 | 567 | macro then*[T](cond: bool, val: T): untyped = 568 | ## When the condition is true the result is ``some(val)``. 569 | ## Otherwise the result is ``none(val.type)`` 570 | runnableExamples: 571 | assert 100.toOpt == true.then(100) 572 | quote do: 573 | if `cond`: 574 | some(`val`) 575 | else: 576 | none(`val`.type) 577 | 578 | --------------------------------------------------------------------------------