Add a safe pattern and special forms for options#8358
Add a safe pattern and special forms for options#8358PMunch wants to merge 1 commit intonim-lang:develfrom
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.
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.
Could you add a runnable example for these procedures?
There was a problem hiding this comment.
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.
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?
|
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.getIt 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. |
| else: | ||
| otherwise | ||
|
|
||
| template either*(self, otherwise: untyped): untyped = |
There was a problem hiding this comment.
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.
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
| ## 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.
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.
@Varriount that is a true criticism. But this is a problem unrelated to this PR.
There was a problem hiding this comment.
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.
I believe these should go on a runnableExamples block.
There was a problem hiding this comment.
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.
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.
Should there be an additional optCmpIt proc, mirroring the *It procedures in sequtils.nim?
There was a problem hiding this comment.
Yeah that would be nice.
krux02
left a comment
There was a problem hiding this comment.
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
| ## 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.
@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.
WTF, what are you thinking here?
There was a problem hiding this comment.
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.
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.
how about this:
template `not`*[T](self: Option[T]): bool = self.isNoneYou 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.
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.
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.
How would this be written then? I just went with the first thing that worked..
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Uh? What does this operation mean?
There was a problem hiding this comment.
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.
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.
There is some already, isn't this redundant?
There was a problem hiding this comment.
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.
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.
@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.
Why is this needed?
There was a problem hiding this comment.
See my other comment
| 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.
Same here, people can use isSome explicitly, no need to circumvent type safety
There was a problem hiding this comment.
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..
| else: | ||
| otherwise | ||
|
|
||
| template either*(self, otherwise: untyped): untyped = |
There was a problem hiding this comment.
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.
@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, " ", b2Then anyone can metaprogram the case statement for their own types. There is a real need to resolve type though like |
dom96
left a comment
There was a problem hiding this comment.
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.