I have been fiddling with Options in Nim.
some(now()) and none(DateTime) look syntactically similar but work fundamentally differently:
The similar form func(...) hides that one takes a value, the other a type.
Proposal: Make the syntax reflect the semantics:
none[] visually looks like an empty box, which matches what it does.
Thoughts?
This breaks the language consistency. none and some are function calls and they work fundamentally in the same way.
If you suggest they remain as such, then they also must allow none[]() and none() forms. Which means they take no arguments (or a default) and the former is a generic. But a generic can't take nothing, there should at least be something like auto inside [].
The other option (speaking syntactically, makes no sense otherwise) is making none and object, but then [] becomes a function call, which is conventionally used for dereferencing, which would be confusing in this case, or element access, which makes even less sense.
The only other option is making none a keyword, which could work, but that would require making Option a part of the language, not just the standard library, and this won't happen anytime soon.
---
Tangentially, Nim allows both to[TypeDesc] and to(TypeDesc) call forms and, for example, in my itertools PR I had an option to choose one or the other forms (or both) for the collect operation, but I've chosen collect(TypeDesc) just to follow the conventions, even though the latter form conceptually allows the excessive .collect[Foo](Foo) usage (haven't check if compiler is happy with it, though).
As usual, conflating Result and Option.
Case in point regarding the keyword addition possibility ;)
Thoughts?
Consider:
func f(): Option[int] = none(int)
Here, the compiler actually has all the information needed to deduce T = int but there's no reasonable syntax to express this - this problem is more generally described here - with first-class support for sum types the problem would go away since you no longer need to reference T at all for constructing none - instead, one can think of it as there existing there's a hidden "general none" type which works for all Option instantiations and that implicitly gets converted to Option[T] and this conversion can be made to infer T.
Such support would be general enough that it can be used in libraries without making none a keyword.
Just use more exceptions and stop the Option-T overuse, then the syntax stops mattering, it's a stupid fad.
Exceptions should be for exceptional things. If something can be often none or a value it should be an option. And from an ergonomic point of view wrapping everything in try ... except ... finally is cumbersome.
For example in raytracing, this should really by an option: https://github.com/mratsim/trace-of-radiance/blob/c1744932/trace_of_radiance/physics/materials.nim#L39-L47
# Lambert / Diffuse Materials
# ------------------------------------------------------------------------------------------
func lambertian*(albedo: Attenuation): Lambertian {.inline.} =
result.albedo = albedo
func scatter(self: Lambertian, r_in: Ray,
rec: HitRecord, rng: var Rng,
attenuation: var Attenuation, scattered: var Ray): bool =
let scatter_direction = rec.normal + rng.random(UnitVector)
scattered = ray(rec.p, scatter_direction, r_in.time)
attenuation = self.albedo
return true
# Metal Materials
# ------------------------------------------------------------------------------------------
func metal*(albedo: Attenuation, fuzz: float64): Metal {.inline.} =
result.albedo = albedo
result.fuzz = min(fuzz, 1)
func scatter(self: Metal, r_in: Ray,
rec: HitRecord, rng: var Rng,
attenuation: var Attenuation, scattered: var Ray): bool =
let reflected = r_in.direction.unit_vector().reflect(rec.normal)
scattered = ray(rec.p, reflected + self.fuzz * rng.random_in_unit_sphere(Vec3))
if scattered.direction.dot(rec.normal) > 0:
attenuation = self.albedo
return true
return false
Metal materials can scatter rays, diffuse cannot. I'm not going to create type ScatterException = object of Exception to model the fact that some material may scatter, some never.
Now the reason why I don't use option here but a var + bool, is perf. When I wrote the code 6 years ago, RVO (Return Value Optimization) wasn't working in this case + option .get returned a copy and changing it to lent had a showstopper bug (https://github.com/nim-lang/Nim/issues/14420) so today it should be as efficient (famous last word). All of that impacted my perf by 20~30%.
Another example from deep learning / AI
Some layer add a bias to matrix multiplication C = A*B + bias and some do not. It's really strange to write
let C = block:
var C = A*B
try
C += bias
finally:
C an option would be way clearer. Exceptions should be for exceptional things.
Nah, they are non-local control flow, use them to make the code shorter. Or don't cause they suck. My point is that the name is misleading as nobody can agree on what "exceptional" means. It is a mechanism that inserts auto-returns into your code. That can be good and it can be bad. But framing it as "it's for exceptional things" isn't particularly helpful.
You can use const to avoid writing none(type) many times.
import std/options
type
Foo = object
x: int
const FooNone = none(Foo)
proc createFoo(x: int): Option[Foo] =
if x == 0: FooNone else: some(Foo(x: x))
var f0 = createFoo(0)
doAssert f0.isNone
var f1 = createFoo(1)
doAssert f1.isSome sure, you can abuse exceptions
Currently Option-T and Either-T are abused much moreso. Forcing every caller to bubble up errors manually isn't the solution when systems keep growing faster than we can handle it.
proc createFoo(x: int): Option[Foo] =
if x != 0: return some(Foo(x: x))