-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a safe pattern and special forms for options #8358
Conversation
Can't we just overload |
Or I like @dom96 suggestion on IRC to call it |
Some discussion here: https://irclogs.nim-lang.org/18-07-2018.html#13:07:37 |
Relevant: Also linked to RFC https://github.com/nim-lang/Nim/issues/7476 |
Ah didn't know |
It would be nice if |
Most of the IRC discussion seems on point — the logical macros are 🤷♂️ but the case one is pretty good. I dislike the names though and hope we can find something better. This kind of thing is why I think a generic pattern matching construct would be so awesome, ideally in Nim itself as an upgrade to |
lib/pure/options.nim
Outdated
mVal = genSym(nskLet) | ||
validExpr = newDotExpr(mVal, ident("isSome")) | ||
valueExpr = newDotExpr(mVal, ident("unsafeGet")) | ||
justClause = nnkStmtList.newTree( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would using quote
make this more readable?
lib/pure/options.nim
Outdated
) | ||
|
||
proc `>>=`*[T,U](self: Option[U], p: proc(value: U): Option[T]): Option[T] = | ||
## Used for chaining monadic computations together. Will create a none if |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a runnable example for these procedures?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've completely rewritten most of it now, and it's a lot clearer. Tried to add runnableExamples, but it messed up when using macros so I converted them to code-blocks for now.
@citycide Neither nesting nor multiple patterns per branch are an instrinsic limitation. In fact, everything that is listed on the README of Patty as currently missing is doable with macros only. It would also be nice to include this macro to support options. It is just that I could not find the time to work on these other features. Nesting is probably the main one, because it requires the macro to become recursive. After this restructuring is done, adding different kind of base patterns, such as options, arrays and seqs, should be doable easily |
Okay, I've completely rewritten most of this now. It has a There is one issue in that Nim refuses my overload of |
lib/pure/options.nim
Outdated
reset() | ||
|
||
check genNone() or genNone() or "c" == "c" == true | ||
check sideEffects == 2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you put braces ()
here? I have no idea how the AST is constructed with the two ==
operatiors. The or
operator has lower operator precedence that ==
. This is what is confusing me. And please also with the other tests.
EDIT:
according to the parse tree, this is the AST: check((genNone() or genNone()) or (("c" == "c") == true)))
are you sure you want to test that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just updated now
Just wanted to mention that using Nevertheless, there is an overlap here - options may be just another pattern that patty recognizes. What should we do? [ ] Rename match in patty and have two different forms of pattern matching for options vs anything else |
Same remark as #8369:
|
@mratsim, it's not possible without modifying the internals though to create new non-symbol operators.. Having some confusion for @andreaferretti, we can change the name if people want. |
@PMunch I have nothing against match, but if we introduce pattern matching for options in stdlib it may make sense to introduce a more general construct |
Ah, that makes sense. This isn't quite pattern matching though. It is just a wrapper around an if and a case statement. All the actual matching is done by the case statement, which I guess is already a general construct. match ourOption:
some _ of 0..high(int): echo "It's huge!"
some x: echo "It's a measly ", x
none: echo "It's nothing at all" would get converted to: let tmp = ourOption
if tmp.isSome:
case tmp.val:
of 0..high(int):
echo "It's huge!"
else:
let x = tmp.val
echo "It's a measly ", x
else:
echo "It's nothing at all" |
@PMunch well, that's exactly what patty does - rewrite into a case statement - but for other types of values otehr than options (objects and variant objects) |
@andreaferretti ah interesting, I'm not really qualified to answer your question though. I'm happy as long as we get a safe and easy pattern for options. The best case would of course be if we could shadow an identifier as well. So if a user tried to do something like this: let noneOpt = none(int)
match noneOpt:
some _: echo noneOpt.get
none: echo noneOpt.get It would actually throw an error instead of compiling and later throwing a runtime error. @bluenote10 forgot to answer this. The matching didn't support that when you asked, but they do now. |
lib/pure/options.nim
Outdated
@@ -139,6 +266,12 @@ proc get*[T](self: Option[T], otherwise: T): T = | |||
else: | |||
otherwise | |||
|
|||
template either*(self, otherwise: untyped): untyped = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this takes an untyped statement, does it follow short-circuit logic? That is, if self
is none, otherwise
won't be evaluated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, not currently. It's just a wrapper for get
with a default value. But I think it should be changed to do that instead. Makes more sense when seen along with or and and.
lib/pure/options.nim
Outdated
## the type contained in the option. | ||
## the result will be `Some(x)` where `x` is the string representation of the | ||
## contained value. If the option does not have a value, the result will be | ||
## `None[T]` where `T` is the name of the type contained in the option. | ||
if self.isSome: | ||
"Some(" & $self.val & ")" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this work properly with strings? I seem to recall that addQuoted
might need to be used in these cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Varriount that is a true criticism. But this is a problem unrelated to this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, not entirely sure. That is one of the few things I haven't changed in this module. Only change I did there was to change the wrapping of the comment to observe 80 chars.
lib/pure/options.nim
Outdated
## If nothing is returned from ``statements`` this returns nothing. | ||
## | ||
## .. code-block:: nim | ||
## echo some("Hello").?find('l') ## Prints out Some(2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe these should go on a runnableExamples block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I originally had them as runnableExamples
but it got confused and produced some really weird code from it that failed spectacularly. Try and change them and you'll see.
lib/pure/options.nim
Outdated
## Basic pattern matching procedure for options. Converts the statements to a | ||
## check and a ``case`` statement. The structure of the matching body is as | ||
## follows: | ||
## ..code-block:: nim |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe these should go on a runnableExamples block.
lib/pure/options.nim
Outdated
#echo result.repr | ||
|
||
proc optCmp*[T](self: Option[T], value: Option[T], | ||
cmp: proc (val1, val2: T): bool): Option[T] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there be an additional optCmpIt proc, mirroring the *It procedures in sequtils.nim?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that would be nice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally I have to say, I don't like this PR at all. I am sorry to say this, because you probably invested a lot of time in it. I can see that, but overall it feels lake a big hack that tries to make everything compile to something. It really circumvents the safety net that static typing normally brings.
lib/pure/options.nim
Outdated
## the type contained in the option. | ||
## the result will be `Some(x)` where `x` is the string representation of the | ||
## contained value. If the option does not have a value, the result will be | ||
## `None[T]` where `T` is the name of the type contained in the option. | ||
if self.isSome: | ||
"Some(" & $self.val & ")" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Varriount that is a true criticism. But this is a problem unrelated to this PR.
lib/pure/options.nim
Outdated
optCmp(self, value, proc (val1, val2: T): bool = val1 == val2) | ||
|
||
template `not`*[T](self: Option[T]): Option[T] = | ||
## Not always returns a ``none`` as a ``none`` can't implicitly get a value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WTF, what are you thinking here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It started as a workaround in that x != y
get's rewritten to not x == y
which breaks the logic of maybes. But when you think about it what would the not
of a none
be? You can't create a some
out of it as that would imply it has a value..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about ==
should not return an option in the first place? It should return a bool
, always. You create a situation where both x == y
and x != y
both can have the same value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how about this:
template `not`*[T](self: Option[T]): bool = self.isNone
You don't really need to keep the type T, because the value is either thrown away, or you need to come with something that is true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think that entire idea of not(Option[T])
smells bad. If you go this way, please also redefine and/or. And move it into separate module. Separate of stdlib. Main options.nim should return "==" result as bool, and avoid extending standard language functions with new meanings. Such level of changes to the language should be done after thorough discussion rather than on the spot.
lib/pure/options.nim
Outdated
|
||
template `==`*[T](self: Option[T], value: Option[T]): Option[T] = | ||
## Wrapper for optCmp with ``==`` as the comparator | ||
optCmp(self, value, proc (val1, val2: T): bool = val1 == val2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is an unnecessary lambda expression. Lambda expressions have a bad performance penalty, because they can never be inlined. Comparison is something that is very likely to benefit from inlining.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would this be written then? I just went with the first thing that worked..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are so many problems here. I think the biggest problem is that ==
returns an Option type. It shouldn't. And for performance, you just inline the comparison. You can do it with a template if you want code duplication.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if self.isNil:
value.isNil
else
value.isSome and self.val == value.val
lib/pure/options.nim
Outdated
check sideEffects == 2 | ||
reset() | ||
|
||
check ((genNone() or genSome()) or "c" == "b") == true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be a compile error
((genNone() or genSome()) or ("c" == "b")) == true
here you resolve the operator proc `or`(x:Option[string]; y: bool)
. This operator should not exist, because the result type is unclear.
Another reason why this should not compile is. It it would not have compiled, you would have noticed this unwanted behaviour. Don't try to make every expression valid somehow. When you do that it will be very hard to find unwanted behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's a fair point. The problem here is that options are implicitly boolean and values are implicitly some
to make them more ergonomic to use. As I mentioned in my first comment after the rewrite I think the comparators need a different symbol, maybe ==?
, !=?
, etc. That would also make this fail as you would use ==?
to compare the two "c"-s
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have an idea. How about you implement all these operators in a nimble module. There you can experiment as much as you want without stepping on anybodys foot. Nim really doesn't need you to have write acces to the options module to exend that type with behavior.
lib/pure/options.nim
Outdated
check sideEffects == 2 | ||
reset() | ||
|
||
check ((genNone() or genNone()) or "c" == "c") == true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could have written it above, but here it is equally true. Why would you want to ever test _ == true
? In my opinion that should never be necessary or meaningful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes more sense if you consider that or
and ==
are overloaded here. If they had different names it would be more obvious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The right operator is of type boolean, so the reader expects that the left operand is also of type boolean and that there is absolutely no overload happening. But you just break that assumption and convert the right operator with a conversion proc into an option type. This is one reason, why converter procs are a bad thing. They hide what is actually going on in the code.
lib/pure/options.nim
Outdated
test "conditional continuation": | ||
when not compiles(some("Hello world").?find('w').echo): | ||
check false | ||
check (some("Hello world").?find('w') == 6) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should not compile. some(6)
and 6
are different types. They are not equal, and therefore should not be equal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All values are implicitly a some
from the converter, which is why this works.
lib/pure/options.nim
Outdated
|
||
converter toSome*[T](value: T): Option[T] = | ||
## Automatic wrapping of values into ``some`` | ||
some(value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think using converters is a good idea here. T
and option[T]
are different for good reasons, I prefer excplitly writing some(x)
if necessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I wasn't sure about that either and it seems to already cause some confusion.. The idea was to reduce the effort of using options but I guess it might be better to err on the side of being explicit here.
lib/pure/options.nim
Outdated
y | ||
else: | ||
none[U]() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uh? What does this operation mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's explained in the "Logic operations" part of the tutorial. and
and or
return an option when all, or one of it's arguments is an option respectively. and
requires both options to be some
and returns the last one, or none
if one them isn't a some
. or
requires one of the options to be some
and returns the first one that is. This means you can implement full boolean logic through only combining options.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand what these operations do. I don't understand what they mean, unlike - say - their boolean counterparts.
lib/pure/options.nim
Outdated
## Procedure with overload to automatically convert something to an option if | ||
## it's not already an option. | ||
some(value) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is some
already, isn't this redundant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it has two versions, one that take an option and one that doesn't. This means that if the value is an option it will not be wrapped in a second option, but if it's not an option it will be converted into one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nim is statically typed, so I don't see in which situation you may not know whether you already have an option or not
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@andreaferretti Nim also supports generic programming (including templates), where you may need to support both types with the same code
lib/pure/options.nim
Outdated
proc toOpt*[T](value: Option[T]): Option[T] = | ||
## Procedure with overload to automatically convert something to an option if | ||
## it's not already an option. | ||
return value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my other comment
lib/pure/options.nim
Outdated
converter toBool*[T](opt: Option[T]): bool = | ||
## Options use their has-ity as their boolean value | ||
opt.isSome | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, people can use isSome
explicitly, no need to circumvent type safety
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah it seems like this is just causing confusion so I think it's best to just remove the converters.. Or at least hide them behind a flag..
lib/pure/options.nim
Outdated
@@ -139,6 +266,12 @@ proc get*[T](self: Option[T], otherwise: T): T = | |||
else: | |||
otherwise | |||
|
|||
template either*(self, otherwise: untyped): untyped = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In many languages Either
is another data type, related to Option. Using either
here can be confusing, and it seems not necessary, since it is just an alias for get
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@krux02 asked if this would evaluate other if it was a procedure, and I think that it makes sense if it's rewritten to not do that and keep both either and get with default. This means you can do `either(, procedureWithSideEffects()) and only if nothing comes of the options statement then will the side effects procedure be run.
I have to say I am not very convinced by this PR. It seems to add 4 things:
My suggestion would be to split this PR into smaller ones that can be discussed independently - I wouldn't like to accept this in toto |
@andreaferretti, yeah I think the converts will have to go. They seem to cause more trouble than what they're worth. The utility functions you seem to like are already in the options module, not something that I've added. The and/not/or and comparators are new, and they create a powerful set of tools to work with options, but I feel that the comparators should change name. I would like to do the same with |
Well that arrow notation seems completely arbitrary to me. At least come up with a great syntax please when you push for this non-idiomatic feature. ;-) |
Let's start with pattern matching for case as everyone seems to agree on it. Then we can think about more syntactic sugar. I like |
I, for one, do not agree in having pattern matching for case. Not that I would dislike it, but doing it with a macro is a great opportunity to show the extensibility of Nim. It would be really nice to have some form of pattern matching - more general than patty - in the standard library as an importable macro, rather than baking it into the compiler |
@andreaferretti, iirc the goal is for Araq to add See this example for forLoopStatement: macro enumerate(x: ForLoopStmt): untyped =
expectKind x, nnkForStmt
# we strip off the first for loop variable and use
# it as an integer counter:
result = newStmtList()
result.add newVarStmt(x[0], newLit(0))
var body = x[^1]
if body.kind != nnkStmtList:
body = newTree(nnkStmtList, body)
body.add newCall(bindSym"inc", x[0])
var newFor = newTree(nnkForStmt)
for i in 1..x.len-3:
newFor.add x[i]
# transform enumerate(X) to 'X'
newFor.add x[^2][1]
newFor.add body
result.add newFor
for a2, b2 in enumerate([1, 2, 3, 5]):
echo a2, " ", b2 Then anyone can metaprogram the case statement for their own types. There is a real need to resolve type though like |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I think a lot of the discussion here has focused on pattern matching so far. This PR does a lot more than just that, and as it stands I would reject all of it except the .?
macro.
Can we close this PR and create a new one with this macro? We can then discuss pattern matching in a separate issue/forum thread (I strongly suggest the latter).
@dom96 just close it. |
Oh wow, I've been away for two weeks on vacation and there are a lot of replies to go through. I'm still polishing my article on why this way of treating options is a good idea. But it's mostly about the concepts, the usage of |
I've now finished up my article on options, and why I think adding all of these are a good idea: https://peterme.net/optional-value-handling-in-nim.html |
I have read @PMunch article, and I am still unconvinced. For one, the reason for introducing About Finally, the comparators like So in the end I still think that the best course of action is to extract the |
The implementation of let name = userTable.getOpt("name") or some("John Doe") This could of course be done with inline if statements in Nim:
But in my opinion this is a bit on the verbose side, and it shows the check-blindness concept I mentioned in the article. If I mess up and put the Now that does little more than what the proposed
Or if we wanted some error checking and wanted to use the matcher:
I share your concern with The scepticism around the comparators is also fair, they are more of a niche feature and as I mention in the article they are more there to flesh out the concept and make it a bit easier to work with options. I implemented |
Just a small note: you may have not noticed I have issues with |
Yeah I agree that |
Hi folks. I'm the BDFL of Toccata. I'm gobsmacked that anyone would think an idea of mine is interesting enough to include in another language. And your discussion here has given me some food for thought. I'm not that knowledgeable about Nim, but I've been intrigued by it for some time. Happy to answer any questions anyone has about Toccata, though. :) |
@jduey Can you please explain to me why |
I can try. I don't see it as 'int' not losing any info as much as 'bool' loses all info except for a single bit. An 'int' still has a magnitude and 32 or 64 bits of info. If a programmer wants an int to carry more info, such as units, they can wrap it in a type. There's also a question of how big a step to take at once. Eliminating bools was a small enough step I could feel confident in taking it. Eliminating unadorned ints is a much bigger step. And now that you mention it, I might have to put some thought into that. |
Oh hi @jduey, nice to see you here :) @Araq, I mentioned this in my article. IMHO boolean-blindness is, as you mention, a bit silly. All values lose information, it's not special for booleans. I also link to another article that points this out and generalizes the problem to something he calls part-blindness. This concept is however a bit too broad and is dealt with in Nim in other ways. But back-tracking to boolean blindness most people tend to talk about it when performing checks. Things like: if x.isNil:
echo x[]
else:
echo "X is nil!" Now this obviously won't work, as I've switched the if bodies around by "accident". This is what I refer to as check-blindness in my article, the fact that the context of the boolean check is lost. By using options and the special case match some(x) !=? some(nil):
some y: echo y[]
none: echo "x is nil!" This shows that the context of the check is not lost, it lives alongside the option, and the value we care about can only be used when it is valid. PS: I'm planning on adding overloads to the postfixed comparators and the right hand side of the |
Okay, did some more rewrites. Now you can use all the logic operators and comparator operators with the right-hand value being a value and not an option. Just means you don't have to put
require ["abc".find('o'), "def".find('f')]:
some [firstPos, secondPos]:
echo "Found 'o' at position: ", firstPos, " and 'f' at position ",
secondPos
none: echo "Couldn't find either 'o' or 'f'" IMO this is more convenient than |
Sorry, but I don't understand the rationale for this complete rewrite. Hundreds of lines of code are using I'm happy to accept a pattern matching macro (and the |
What? This isn't a complete rewrite.. I had to force push over my copy after I rebased against the current devel. The deprecation has been there from the start (or at least close to it).. Could of course remove it, but since no-one mentioned it I thought people were fine with it. The reason to deprecate it is to let people use the more powerful constructs that avoids silly runtime errors. As I mentioned in my last comment I removed the pattern matching part of What about the new wrapping macros? Yay or nay? And what don't you like with the comparators and logic operators? |
Got tired of discussing this since it was basically just the same couple arguments over and over. So I removed everything but |
Split options into |
Had to un-fuck the git history of this PR. I made some mistakes when trying to make it up to date with |
This adds the safer pattern of access from superfunc/maybe, along with adding the maybe logic defined in Toccata to avoid using boolean expressions alltogether.