I implemented two optimizations for the Option type. The idea was to do the same as the Rust compiler, use the nil/nullptr for the none option without occupying extra memory. Then I had a similar idea for Option[Natural], where I could use -1 internally to store none. That type could be used for the find procedure so that it does not return int anymore, but Option[Natural] without overhead.
Here is the code, maybe if there is interest this might become a pull request. The think I don't like about this implementation is, Option[Natural] initializes to Some(0), and not None.
Here is the code:
import typetraits, macros
type
PointerType = ptr|ref|string|seq
Option*[T] = object
## An optional type that stores its value and state separately in a boolean.
when T is Natural:
val: int # use -1 for none
elif T is PointerType:
val: T # use nil for none
else:
val: T
has: bool # fallback to use an extra field
UnpackError* = ref object of ValueError
proc some*[T](val: T): Option[T] =
## Returns a ``Option`` that has this value.
when T is Natural:
result.val = int(val)
elif T is seq or T is string or T is ptr or T is ref:
assert val != nil
result.val = val
else:
#assert(false == T is seq or T is string or T is ptr or T is ref)
result.has = true
result.val = val
proc none*(T: typedesc): Option[T] {.inline.} =
## Returns a ``Option`` for this type that has no value.
when T is Natural:
result.val = -1
elif T is seq or T is string or T is ptr or T is ref:
result.val = nil # does nothing just for clarity
else:
result.has = false
proc option*[T,U](t: typedesc[T], val: U): Option[T] {.inline.} =
when T is Natural:
result.val = max(-1, val)
elif T is seq or T is string or T is ptr or T is ref:
result.val = val # does nothing just for clarity
else:
result.val = T(val)
result.has = true
proc isSome*[T](self: Option[T]): bool {.inline.} =
when T is Natural:
self.val >= 0
elif T is seq or T is string or T is ptr or T is ref:
not self.val.isNil
else:
self.has
proc isNone*[T](self: Option[T]): bool {.inline.} =
when T is Natural:
self.val < 0
elif T is seq or T is string or T is ptr or T is ref:
self.val.isNil
else:
not self.has
proc unsafeGet*[T](self: Option[T]): T =
## Returns the value of a ``some``. Behavior is undefined for ``none``.
assert self.isSome
self.val
proc get*[T](self: Option[T]): T =
## Returns contents of the Option. If it is none, then an exception is
## thrown.
if self.isNone:
raise UnpackError(msg : "Can't obtain a value from a `none`")
self.val
proc get*[T](self: Option[T], otherwise: T): T =
## Returns the contents of this option or `otherwise` if the option is none.
if self.isSome:
self.val
else:
otherwise
proc map*[T](self: Option[T], callback: proc (input: T)) =
## Applies a callback to the value in this Option
if self.isSome:
callback(self.val)
proc map*[T, R](self: Option[T], callback: proc (input: T): R): Option[R] =
## Applies a callback to the value in this Option and returns an option
## containing the new value. If this option is None, None will be returned
if self.isSome:
some[R]( callback(self.val) )
else:
none(R)
proc filter*[T](self: Option[T], callback: proc (input: T): bool): Option[T] =
## Applies a callback to the value in this Option. If the callback returns
## `true`, the option is returned as a Some. If it returns false, it is
## returned as a None.
if self.isSome and not callback(self.val):
none(T)
else:
self
proc `==`*(a, b: Option): bool {.inline.} =
## Returns ``true`` if both ``Option``s are ``none``,
## or if they have equal values
(a.isSome and b.isSome and a.val == b.val) or (a.isNone and b.isNone)
proc `$`*[T]( self: Option[T] ): string =
## Returns the contents of this option or `otherwise` if the option is none.
if self.isSome:
"Some(" & $self.val & ")"
else:
"None[" & T.name & "]"
when isMainModule:
import unittest, sequtils, macros
suite "options":
# work around a bug in unittest
let intNone = none(Natural)
let stringNone = none(string)
test "has has":
var
a: Option[int]
b: Option[Natural]
c: Option[string]
d: Option[ptr int]
e: Option[ref int]
f: Option[seq[int]]
check( compiles(a.has) )
check( not compiles(b.has) )
check( not compiles(c.has) )
check( not compiles(d.has) )
check( not compiles(e.has) )
check( not compiles(f.has) )
test "example":
proc find(haystack: string, needle: char): Option[Natural] =
for i, c in haystack:
if c == needle:
return some Natural(i)
none(Natural)
check("abc".find('c').get() == 2)
[...]
I'd love an Option[T] without overhead, was just looking for this.
I'll just use "nil" for now but that would be better.
I like the idea. It would be nice if it was also specialized for ref not nil and ptr not nil where nil would map to none. Just like in Rust. :D
In fact, it would be cool if functional style was generally more common in Nim. Like standard library widely utilizing option where possible. Also: sequtils only work for seq, it would be nice to have similar functionalities for any iterable (as for now, there is a non-official package iterutils). Et cetera...