Much ado about nil #339
Replies: 6 comments 16 replies
-
So, first things first: I am however not against a way to easily place asserts and have asserts change the type to strip away the Instead I think we need a solution that grabs all values and checks if the first indicates a fail. If it is a fail, return everything it captured (in the same order!) out of the current function. Otherwise, make the code behave as if this check never happened (Except that the The problem however is that lua (to my knowledge) can't easily return when in an expression. Nor would it be easy to check and return the value. As a result, significant code changes need to happen between teal and the generated lua. However, I am convinced that introducing 1 character to the syntax that signals "return early if there is an fail value" is still ergonomic. This comes from writing rust which uses As a result, my suggestion is to do it like function b(): number | nil, string
local? a = somePossibleFailingFunction "some argument"
return a + 10
end Which would turn into this teal code (better transformation suggestions welcome) function b(): number | nil, string | nil
local _a = {somePossibleFailingFunction "some argument"}
if not _a[1] then
return table.unpack(_a)
end
local a = table.unpack(_a) as number
return a + 10
end Making this operator only 1 character longer is effectively the same as introducing the an operator of 1 character. So, it passes the "is it as easy to type as Rust" test (Rust also returns errors rather than throws them and uses It also makes sure that it can only be used on a location where minimal code transformations are needed. The other suggestion I want to make is to, at least for the time being catch using It also for the time being makes those happy who (like me) rather have more verbose code and know that there isn't a Doing it this way teal can also always fall back to "You can enable this warning or even make it a compile time error, but that is on you. Providing syntax to deal with it is out of scope" in the case that there is no good solution and have the warning be disabled by default. |
Beta Was this translation helpful? Give feedback.
-
I wrote about this earlier. #71 (comment) Basically, bolting on non-nullable types too late introduces breaking changes that are difficult to work around. There are also implications in Lua as to how ipairs should work and if there should be fixed size arrays. I still think it is extremely important to have nilable types, because that means one class of extremely common error can be dealt with effectively. |
Beta Was this translation helpful? Give feedback.
-
My opinions on this might be a bit loaded. After all, my preference is ML-like type systems, as used in Haskell, Amulet... My experience with C-like types is close to none, so I am definitely biased, but I will try to assess the problem the best I can. To avoid an XY-type of situation, I think it's important to look at what we're trying to solve here. Operations sometimes fail, and it is natural of a language to have a way of representing this. As @euclidianAce said, in Lua, this is done by returning nil, and maybe even an error message sometimes. In other languages, you have something like I am personally not a fan of
I think our goal here should be ensuring that dealing with types stays in the type system, and code stays at runtime. I think we all definitely agree that Here is where things get spiky: finding a solution that not only works for everyone, but everyone likes too. Teal is still a Lua dialect, and I should not forget that. Here is my attempt at not forgetting that Lua is Lua, and not Haskell. As is, Teal is bound to the Lua idiom of returning
I would like to ensure that Teal users are able to use it both ways, or even a third way! I'm open to surprises. That being said, let's look at the proposed solutions:
As stated in several points above, I don't think runtime checking is the way to go. Asserting is still possible without a language construct, and if that's how users wish to handle their errors, they are already able to. This is why I don't believe this is a solution at all, even we made
I have several points to make about this:
Agree! Despite what my last points may suggest, I am not against making assert something easier to use, simply against it being a recommended way of doing it at all.
This solution has two very serious issues in my opinion. For starters, and as I mentioned earlier, this is just making a Lua idiom a solution to something that should remain open to every way of handling errors. For example, this would not fit my use case at all. (Remember! I still want to be able to use option types). Secondly, even for the users that do want to handle it similarly, it is completely rigid. What if I want to change the error message (assuming that we called a function that returns
Again, what happens to the other cases? The ways in which errors can be handled in Lua are almost endless, and that's one of its strengths. If we limit the places where this can be used, not only I predict this would be quite bothersome to implement, but only apply to a minimal set of your users.
Yes! Definitely. Languages like Haskell, for example, tell you when you have not listed out all possibilities exhaustively and covered them. This will be a point in my proposed solution (if I ever get to it, because as of the time I'm writing this sentence, I've been writing for almost two hours). Now, before getting to my solution(s) proposal(s), I want to comment on some very real problems we could have in the implementation of this. You (reader) are getting close though, because my comments on these problems will be directly linked to my solution(s).
What a great point! This is something that honestly I would have forgotten about while drafting my comment. My opinion here is that the types should describe what the variable is without magic. If
Effectively, whatever the solution is, it has to be the following:
Now, all of that said... let's get into the solutions I propose. I will do this by putting blocks on top of blocks. I will try to start from the core of this issue, to the final abstraction and solutions. Our first instinct is to let Teal express A divide, a gap comes in. We either fill everything with This proposed set of solution imagines a Teal that can use both option types and nullable types, map between them, and make some available to Lua. The solution will be written in several points, so that people who reply to this can refer to these points and parts of the solution(s) easily.
If we did not want this to be the only solution, it doesn't have to be. I am favorable to adding the following, right alongside my previously proposed solution.
But for a moment, I want to talk about the future of Teal. A point was raised in the Gitter that straying too far away from Lua may make Teal not just "typed Lua". This part is purely opinion, you can stop reading if you were only looking for my proposed solution to this. It is true that adding things like option types, and other things in the future may create a gap between thinking in Lua and thinking in Teal, and we do not want to make a split in an already small community that is Lua. But I am of the opinion that Teal could be its own language with its own constructs, while still fully working with Lua and being similar. Maybe Teal won't be all Lua and I think that's fine. I can imagine an ecosystem where Teal modules interact with other Teal modules, Teal rocks are distributed to be used in other Teal projects. Now, imagine all of these libraries were developed with Lua in mind: we wouldn't export types, or functions that rely on types, or anything-types really. We would be limiting or even hindering Teal<->Teal interoperability in the name of Teal<->Lua interfacing. At some point, a Teal library that only makes sense to be used in Teal will come because of the types it provides. At some point, a user might want to work with Teal more than they want to work with Lua. I think the future of Teal lies in being open to using the language however we wish. If you want it to be just typed Lua, nobody will stop you, but if we have more and better features than just a typed Lua (and never splitting away from Lua), then the reasons to make the switch to Teal become more than just type checking my own code. I don't regret spending 4 hours on this. |
Beta Was this translation helpful? Give feedback.
-
I'll jump in and add a few thoughts of my own:
I personally don't mind the idea of using union-types all over the code, if there's good enough syntax for dealing with this. Firstly, I'd appreciate some syntax to shorten As for how to deal with A more realistic approach might be to just provide a simpler mechanism of checking for
The keyword |
Beta Was this translation helpful? Give feedback.
-
Ok, sorry for necro but... as no one has a good idea on how to move forward with First of: The distinction between Right now, unions This would make discriminated unions (like from pr #124 ) possible without it being a language feature. Once the current std types are moved over to properly deal with optionality we have also lowered the bar for people to experiment with new ideas on how to properly warn the user that a nil check is missing. It also allows it to be implemented in small steps. So for example instead of always going "hey! you forgot the nil check." maybe it starts out to complain just when you try to get a field out of it or something along those lines. So, to recap: So, lets start with changing the std types to incorporate optionality and start using this difference even if due to syntax concerns it can't yet be used to block users from forgetting nil checks. |
Beta Was this translation helpful? Give feedback.
-
There's tons of great thoughts in this thread but I don't want it to blow up exponentially by commenting on each individual bit, so I'll just do a quick recap on my top 2(+1) concerns regarding true non-nilable types in Teal:
The first two are language design issues, the bonus one is more of an implementation worry. |
Beta Was this translation helpful? Give feedback.
-
I'll try to kick this off here with my understanding of how the current type system handles
nil
, and some proposed alternatives.Currently,
nil
is a type in Teal, but it is almost useless since all types are "nilable", i.e. For any typeT
,nil <: T
(where<:
is the subtype relation). For those that like type theory,nil
is a unit type (but not a bottom type as Teal really doesn't have those, at least, not in a meaningful way). This is one of the first things that newcomers comment on and wonder why this is.Other than being a bit easier to implement, many Lua idioms rely on values maybe being nilable. More specifically, from the 5.4 reference manual
So any operation that can
fail
returns a union with whatever typefail
is, which for now isnil
.A few examples from the standard library returning a nilable type/
fail
tonumber: function(value: any, radix: number): number
: can returnnil
to signify that the input could not be converted to a numberio.open: function(file_name: string, mode: string): FILE, string
: can returnnil, string
to signal that Lua was unable to open the file along with the reason whyload
,string.find
, and many more follow this pattern of returningnil
on failurereturns fail
So whats the big deal? We can just modify the stdlib type definitions to be like
tonumber: function(any): number | nil
and make the programmer deal with it by manually checking the result.And, yeah, we can. I think that the issue of "make types non-nilable" is actually quite easy to solve. In fact I think we could just delete an if statement in the
is_a
(Basically the internal representation of the<:
subtype operator) function intl.tl
to not return true if the lhs isnil
. The real issue that people would then have with this is that you shouldn't be allowed to do anything withnil
or any type that is a union withnil
withoutunwrap
-ing it first.So the issue moves from "all types are nilable" to "how can we conveniently unwrap unions with nil in a way that doesn't suck?"
Some proposed solutions:
assert
never return anil
type, i.e. make the signatureassert: function<T>(T | nil, string): T
!
that takes a value and 'unwraps' it as described above and is compiled to a call toassert(value ~= nil and value, "Expected value to be non-nil")
Optional<T>
,Maybe<T>
,Nilable<T>
,Failable<T>
, etc. generic with somevalue()
/unwrap()
/just()
"method" that would basically be compiled to anassert()
What are the problems with these? Well it's not in the solutions themselves, but with how often you'll have to use them.
For example, table indexing can, in general, return
nil
. Given a dynamic arrayarr
of type{number}
(in this scenario we treatnumber
as non-nilable), what should the type ofarr[5]
be? With no other context, the best we can assume isnumber
ornil
. I think the question then becomes, if indexing, in general, is nilable, how can the programmer avoid this annoyance of having to unwrap this union every time they index into a table. (Additionally, we should also assertarr
is non-nil before indexing into it, but thats not the point here)Lua kind of expects
nil
to be everywhere and trying to teach the type system about that without nilable types is non-trivial and designing an experience of dealing with non-nilable types in an environment that has lots of nilable types is also non-trivial.So any solution to this has to be very, for lack of a better word, ergonomic. Or else using Teal will just kinda suck.
If non-nilable types do happen, one consequence of that will probably be the inference of this kind of if statement
Now ideally one thing I'd like to work on is improving flow inference so we can do something like
But this is definitely more complex
Feel free to use this as a thread to propose and (constructively) criticize solutions :D
cc @lenscas @daelvn
Beta Was this translation helpful? Give feedback.
All reactions