Currently there are couple of ways to define optional arguments, but none looks good. Any plans for improvement?
import options
# 1 Nil
# proc somefn(v: int = nil) = echo v # doesn't work for primitives
# 2 Overload, works but is inconvenient
proc somefn(v: int) = somefn(v.some)
proc somefn() = somefn(int.none)
proc somefn(v = int.none) =
echo v
# 3 Option
proc somefn(v = int.none) = echo v
# somefn(1) # doesn't work
somefn(1.some) # works but is inconvenient
# 4 converters
converter to_option*[T: int](v: T): Option[T] = v.some
somefn(1) # works and looks good
# But converters cause problems
proc to_json[T](v: T): string =
when compiles(v.to_json_hook): v.to_json_hook
else: $v
proc to_json_hook[T](self: Option[T]): string =
if self.is_some: to_json(self.get) else: "null"
echo to_json(1.some) # Infinite loop, because of converter
Basic data types, plain object etc. cannot be nil
Yes, and it's a good thing (would be even better if ref also couldn't be nil unless explicitly specified).
But this introduces nilability to value types, which completely breaks all all assumptions.
No, it doesn't introduce nullability to value types, it introduces union type int | null. It doesn't affect the int type, it still can't be nil.
type Defaulted = distinct char
const noVal = Defaulted('0')
proc somefn(v: int | Defaulted = noVal) =
when v is Defaulted:
echo "defaulted"
else:
echo "explicitly"
somefn()
somefn(12)
You can already write it like this. I don't think using nil for type parameters is a good idea, because A | B = nil IMO still implies that both types are nilable.
Also nim does not have union types (you probably meant something like TS union type), it has typeclasses.
"Very clear what's going on" well not really since nil in Nim means nil pointer and you're using it like a type, but turns out void is the magical thing since the following works:
proc somefn(a = 100, b = 300, v: int | void = void) =
when v is void:
echo "default"
else:
echo v
someFn(v = 10)
someFn()
Looks good, but the code below would fail:
proc somefn(v: int | void = void) =
when v is void:
echo "default"
else:
echo v
someFn(10)
someFn()
proc anotherfn(v: int | void = void) =
somefn(v)
anotherfn(10)
anotherfn()
Error
/usercode/in.nim(13, 10) template/generic instantiation of `anotherfn` from here
/usercode/in.nim(10, 9) Error: type mismatch: got <type void>
but expected one of:
proc somefn(v: int | void = void)
first type mismatch at position: 1
required type for v: int or void
but expression 'v' is of type: type void
I used a varargs the other day partly to do this the other day. When dealing with C api's they all use the sentinel value approach, but it's kind of annoying and not as type safe.
Reading the above comments sounds like Nim is pretty close to solving this problem but runs into a few corner cases. Would Nim 2.0 be possible point to add a "constrained option type" or tweak to the void approach that'd make one of these options cleaner? Can't recall if this was discussed in the Nim 2.0 meta-thread.
Though Error: type mismatch: got <type void> looks like it could be a simple compiler tweak/PR?
Using void to represent Null is not a better idea than using Option[T].
Most typed programming languages are using some kind of Option[T].
Nim should be moving towards not nil in the future anyways.
Most typed programming languages are using some kind of Option[T].
Yah, playing around with it a bit more and using some is pretty common in most other modern languages with Option types. It's really not that bad. I did toy around with adding a convenience function for it (similar to % or %*) to the example.
proc `~`[T](v: T): Option[T] = some(v)
somefn(1.some)
somefn(~1)
let
v1 = 3242
v2 = 42
somefn(~v1)
somefn2(~v1, ~v2)
It's a bit of taste whether overloading a symbol like ~ is worth it. Unfortunately no other languages with Option types that I found used any common symbol for Some. It'd be nice if there was a common one like $ for strings.
Most typed programming languages are using some kind of Option[T].
I would change it to "most old typed languages".
Nim should be moving towards not nil in the future anyways.
TypeScript is not nil always. And has excellent optional arguments.
Typescript just behaves as if everything is boxed in ref types, and also it has dedicated support for union types to be able to work with JS code zoo. <value-type> | nil still does not make sense, it does not convey intentions properly and only confuses two types of values. If writing some pains you so much you can use separate type dedicated specifically for defaulted arguments to mitigate converter issues (unless you also want to serialize optional arguments).
import std/options
type
OptArg[T] = object
opt: Option[T]
converter toOptArg[T](arg: T): OptArg[T] = OptArg[T](opt: some arg)
func noArg[T](t: typedesc[T]): OptArg[T] = OptArg[T](opt: none t)
func isSome[T](opt: OptArg[T]): bool = isSome(opt.opt)
proc withOpt(a: OptArg[int] = noArg(int)) =
if a.isSome():
echo "specified"
else:
echo "defaulted"
withOpt()
withOpt(12)