I'm trying to rewrite some Python code in Nim but I am currently struggling at this codeblock. I have multiple optional Inputs (ints) and need to check whether they are set or not.
def foo(x, y=None):
if y is None:
print("y=None")
else:
print(y)
foo(1, 2)
foo(1)
How would I write this in Nim?
Nim's options gives this abillity, alternatively you could use a specific value like -1 or int.low but that's up to you.
import std/options
proc foo(x, y = none(int)) =
if y.isNone:
echo "y = None"
else:
echo y.get
foo(some(1), some(2))
foo(some(1))
foo()
For someone new to Nim and coming from python, I like your suggestion of using int.low...
proc foo(x: int, y=int.low) =
if y == int.low:
echo "y wasn't given"
else:
echo "y = ", y
foo(1, 2) # y = 2
foo(1) # y wasn't given
int.low on my machine is "-9223372036854775808" and above I'm hoping that would never be passed or the logic would break, general I'd be happy.
Not your use case but in case you have a single optional input (it becomes impractical when optional inputs increase) another option (haha) is to have separate overloads (which usually can share the logic):
proc foo(x: int) = echo “no y”
proc foo(x: int, y: int) = echo “y: “, y
Will probably go with that, always having to write
some(value)
is kinda annoying and since I am just running into this example while writing Python code, another case where a None in Python can be replaced by something else in Nim, is the case where a None stands for a value that has a default in terms of previous parameters. For example:
def constant(size=5, start=0, end=None, value=1.0): # cannot do end=size (in Nim I can)
if end is None:
end = size
result = np.zeros(size)
result[max(start, 0):min(end, size)] = value
return result
constant(10, 7) # array([0., 0., 0., 0., 0., 0., 0., 1., 1., 1.])
In Nim:
func constant(len = 5, start = 0, ends = len, value = 1.0): seq[float] =
result.newSeq(len)
for i in max(start, 0) ..< min(ends, len):
result[i] = value
echo constant(10, 7) # @[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0]
Other examples (and other lesser known features): https://github.com/nim-lang/Nim/wiki/Lesser-known-Nim-features#using-complex-expressions-based-on-other-arguments-as-default-arguments-in-procedures
Note also that in Python I cannot use len (although it is not a keyword) while in Nim I can use it (it is an identifier with multiple overloads). In Nim though I cannot use end (which is a keyword, I could use it surrounded by backticks but is not ergonomic).
You can use type class to pass int or nil. Unlike std/options, which type of value you pass must be determined at compile time.
Please read this for more details: https://nim-lang.org/docs/manual.html#generics-type-classes
proc foo(x: int or typeof(nil); y: int or typeof(nil)) =
echo "Foo"
when x isnot typeof(nil):
echo x
when y isnot typeof(nil):
echo y
foo(nil, nil)
foo(1, 2)
foo(nil, 3)
foo(5, nil)
I think it maps to either nil or Option none type. works better with option. also, u can type: some val (without parentheses) or val.some
I personally think the options module should be in the Nim's prelude, receiving the blessing :-)
int.low is an inferior method, as it becomes a magic value unlike the option which forces you to check. However options as they currently are strike me as limited in the optimization department because internally an Option[int] will be represented as a (has: bool; value: int) in memory whereas the magic value is more concise.
It would be more interesting if we could pass in a range[] to an Option[], and then instead of having the internal has boolean, we internally pick a magic value instead whilst retaining the safety of options.
kinda almost works
import options
type
Option*[T: range] = object
value: int
func contains*[T: range](r: typedesc[T], i: int): bool =
i >= T.low and i <= T.high
func some*[T: range](r: T): Option[T] =
result.value = r
func none*[T: range](t: typedesc[T]): Option[T] =
result.value = T.low.int - 1
func isSome*[T: range](o: Option[T]): bool =
o.value in T
func isNone*[T: range](o: Option[T]): bool =
o.value notin T
func get*[T: range](o: Option[T]): T =
if o.isSome:
return o.value
else:
raise newException(Defect, "Value is none")
proc test(o = none range[10..100]) =
echo o.isSome
test some range[10..100] 10 # true
test() # false
except the dealbracker that you have to specify range type in some constructor, otherwise some would just produce Option[int]. So i dont see how that would really work.I was once thinking of having a DirtyOption type that would use various “weird” values for None.
dirtyNone(int) == dirtySome(int.low)
dirtyNone(float) == dirtySome(NaN)
dirtyNone(char) == dirtySome('\0')
dirtyNone(string) == dirtySome("some invalid UTF-8 maybe?")
dirtyNone(ref T) == dirtySome(nil) # std.options already do this!
Options are a type which wrap your type and add a bool, so when you do some(100) it makes an Option[typeof(100)] with the bool flag set to true to showcase how it works here is our own explanative not to be used implementation below:
type Option*[T] = distinct (bool, T)
proc some*[T](a: T): Option[T] = Option[T]((true, a))
proc none*(T: typedesc): Option[T] = Option[T]((false, default(T)))
proc isSome*[T](a: Option[T]): bool = (bool, T)(a)[0]
proc isNone*[T](a: Option[T]): bool = not (bool, T)(a)[0]
proc get*[T](a: Option[T]): T =
assert a.isSome
(bool, T)(a)[1]
var a = some(100)
assert a.isSome
assert a.get == 100
a = none(int)
assert a.isNone
As you can see when using some we make a (true, a) and when we make a none we do (false, default(T)). The behaviour is similar to that of a reference object without nil and more explicit about the expected behaviour.