haskell / core-libraries-committee

95 stars 15 forks source link

Add the `todo` function #260

Closed MangoIV closed 2 weeks ago

MangoIV commented 5 months ago

Dear Haskell core library committee.

Currently there are multiple ways of describing something as unimplemented

problems of the above solutions

  1. Problems of the functions currently in base:
    • undefined:
    • doesn't give you a warning if it remains in code
    • doesn't read as "this is open, I will definitely implement it soon"
    • a lot to type for "please compiler, be quiet for the second, I will come back to this"
    • error:
    • also doesn't give you a warning if it remains in code
    • may read as "this is open, I will definitely implement it soon" but this is a lot to type
    • even more to type for "please compiler, be quiet for the second, I will come back to this", sometimes even needs paranthesis
  2. external dependencies:
    • just for doing a quick debugging todo, it doesn't seem worth adding an alternative Prelude or even add a dependency just for the sake of this
  3. typed holes
    • they are, by default, errors, not warnings
    • they are very noisy (by default they produce a lot of output)
    • they are slow (by default)

That's why propose a function todo that has the following properties:

implementation of the solution

{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE MagicHash #-}
{-# OPTIONS_GHC -Wall #-}

module Todo (todo) where

import GHC.Base (raise#, TYPE, RuntimeRep)
import GHC.Exception (errorCallWithCallStackException)
import GHC.Stack (HasCallStack)

{- | 'todo' indicates unfinished code. 

It is to be used whenever you want to indicate that you are missing a part of 
the implementation and want to fill that in later. 

The main difference to other alternatives like typed holes and 'undefined' 
or 'error' is, this does not throw an error but only emits a warning. 

Similarly to 'undefined', 'error' and typed holes, this will throw an error if 
it is evaluated at runtime which can only be caught in 'IO'. 

This is intended to *never* stay in code but exists purely for signifying
"work in progress" code. 

To make the emitted warning error instead (e.g. for the use in CI), add 
the @-Werror=x-todo@ flag to your @OPTIONS_GHC@.

==== __Examples__

@
superComplexFunction :: 'Maybe' a -> 'IO' 'Int'
-- we already know how to implement this in the 'Nothing' case
superComplexFunction 'Nothing' = 'pure' 42
-- but the 'Just' case is super complicated, so we leave it as 'todo' for now
superComplexFunction ('Just' a) = 'todo'
@
-}
todo :: forall {r :: RuntimeRep} (a :: TYPE r). (HasCallStack) => a
todo = raise# (errorCallWithCallStackException "Prelude.todo: not yet implemented" ?callStack)
{-# WARNING in "x-todo" todo "todo remains in code" #-}

try it out in a separate module, as such:

module TodoExample where

import Todo

superComplexFunction :: Maybe a -> IO Int
-- we already know how to implement this in the 'Nothing' case
superComplexFunction Nothing = pure 42
-- but the 'Just' case is super complicated, so we leave it as 'todo' for now
superComplexFunction (Just a) = todo

This implementation will work from >= ghc981

impact

on hoogle I currently find 4 functions which this would break, two of which have similar semantics to this one, making it improbable that they will be found in code out there.

Another advantage of this function is that there will be no need of a *-compat package because code that does contain this function is not supposed to live anywhere or compile with more than the compiler than the base version this is going to be shipped with supports.

I will obviously run a proper impact assessment though.

why I think this belongs in Prelude

The main reason I think this belongs into Prelude is because it is not possible to replace this with the same level of simplicity with any other solution

I think this will also have the positive impact of offering a real alternative to dangerous uses of undefined

also look at

rust std's todo! and unimplemented! macros

rendered docs

rendered docs without expanded Examples rendered docs with expanded Examples

some more screenshots

Amendment to this proposal (28 Mar 2024)

After what I consider a very productive and diverse discussion, I think the most prominent conclusion is that this might not make the jump into Prelude all at once. I still want to proceed with this proposal for the following two reasons:

  1. accessibility and visibility as well as usefulness in general (least overhead, least tooling issues, least possibility of divergence, etc.) will be greatest if this is in base
  2. and most importantly: I still want this is Prelude eventually; this seems to be only possible when something has been in base for a long time and one has to start at some point ;)

a new module, Debug.Placeholder Develop.Placeholder

There will be a new module Debug.Placeholder Develop.Placeholder that will contain both the

These implementations include many useful improvements to the function described above, which is really awesome.

The name of the module is justified as follows:

Note: if people think that the proposed namespace is incorrect, I would like to hear convincing arguments and am happy to adjust accordingly.

out of scope for this proposal

While there were some really nice suggestions to make the proposal "more complete", I will consider the following out of scope for this proposal while expressing the strong intention to later add them in a follow-up proposal:

MangoIV commented 5 months ago

Not at all! Sorry, you misunderstand me. My point is that this is a functionality that should be provided at extremely low cost for the programmer. My argument is that every „workaround“ to not having this provided at as low cost as possible increases the likeliness that bad practises are being used. I don’t think that people usually add and remove packages to their cabal files just to do what is equivalent to todo. They’d rather use undefined and error "", at least that’s my impression. So what I’m saying is that in practise, contrary to what is claimed, adding and removing a cabal package for this over and over is not an alternative.

BinderDavid commented 5 months ago

@MangoIV You give the following reasons for not using typed holes for this:

  • they are, by default, errors, not warnings
  • they are very noisy (by default they produce a lot of output)
  • they are slow (by default)

As an alternative it might be possible to special case a named typed hole _todo in GHC which would then by default not have these downsides but the semantics you want? Upsides would be that there isn't an additional definition in base and it is almost impossible to break any code since published code usually doesn't contain typed holes.

MangoIV commented 5 months ago

i think this sounds like an actual alternative. Of course this would mean added code to the compiler, and certainly not two lines, but I think it’s a cool idea!

martijnbastiaan commented 5 months ago

Are you suggesting we add stuff to base, because after editing your cabal file, HLS doesn't start fast enough??

While I personally don't care too much about the restart time of HLS (only one of the five steps mentioned by @MangoIV), I do think the rest of the steps are slightly annoying. You have to context switch to another file, find the relevant lines, modify them, etc. For a typical quick script I find that I need to:

Each of which induces a trip down to the Cabal file. What happens in practice is that I end up writing it in Python. On the one hand you could argue this is a tooling issue (HLS should offer to add the dependencies), on the other hand base is just too limited to write anything practical.

All of this is to say: I don't think @MangoIV's comment on adding a package can be viewed in isolation. It's just another housekeepy thing you need to do even when writing fairly simple code.

Edit: as far as this proposal goes: I'm in favor, though we probably should put it in Debug.Trace for reasons already mentioned by others.

silky commented 5 months ago

All of this is to say: I don't think @MangoIV's comment on adding a package can be viewed in isolation. It's just another housekeepy thing you need to do even when writing fairly simple code.

I don't disagree too much with your analysis, but I do think that solving these problems is a totally different topic, and adding more to base itself doesn't seem, in principle, to be the only (or best) answer here.

As in, I think we're desperately in need of tooling to start new projects in a more convenient way, which would help a lot; but that's a bit of a different issue I think.

tomjaguarpaw commented 5 months ago

I totally agree with @silky here. Personally I like this proposal a lot (although I'm skeptical about exporting todo from Prelude) but I don't think the justifications for including things in base (or Prelude) that "it's hard to depend on packages" or "it's hard to import identifiers" hold any water. They are good arguments for improving tooling though!

jeukshi commented 5 months ago

but that's a bit of a different issue I think.

It is also a different issue because the libraries listed by @martijnbastiaan provide non-trivial functionality, that working code will depend on. That's not the case with todo lib, which you want to get rid of, at some point.

So even if adding it to the Cabal file would be automatic, removing it is another issue altogether (not to mention, adding/removing per development cycle). I agree with @MangoIV that this is just not a viable alternative.

On the other hand, for HLS users at least, I don't think it is too much of a burden to import todo from, say, Debug.Trace. It will end up as an unused import at some point, but this is fine, as dealing with unused imports is a separate issue.

sgraf812 commented 5 months ago

I think @BinderDavid's suggestion from https://github.com/haskell/core-libraries-committee/issues/260#issuecomment-2016429304 is very reasonable, avoids namespace pollution and thus breakage, and probably is not all that hard to implement either.

Bodigrim commented 5 months ago

For a typical quick script I find that I need to:

* Construct some path: `filepath`

* Create a temporary directory: `temporary`

* Find some files: `glob` or `directory`

* Launch a process: `process`

* Trim write space from read lines: `extra`

* Do some basic processing: `unordered-containers`

Each of which induces a trip down to the Cabal file.

@martijnbastiaan but why? Cabal supports single-file scripts fairly well:

#!/usr/bin/env cabal
{- cabal:
build-depends: base, filepath, temporary, directory, process, extra, unordered-containers
-}

main :: IO () 
main = pure ()
martijnbastiaan commented 5 months ago

@Bodigrim Cabal scripts complicate caching on CI. In any case, my main point was that setting up new projects / executables / libraries is already a noisy affair in Haskell. Hence, "just use the todo package" doesn't really do it for me. (I think others have articulated it better than me, and perhaps we can continue this specific discussion somewhere else to not pollute this thread.)

endgame commented 5 months ago

My initial reaction was "sure, do whatever you want, I'm not going to use it", but after @mixphix pointed out that todo-in-base means you can't guarantee a package is free from TODOs, and @Bodigrim pointed out that todo will appear as a valid hole fit for anything, I'm against it. Adding things that require additional compiler magic (to not interfere with other features) seems like a bad idea.

hasufell commented 5 months ago

Cabal scripts complicate caching on CI.

Again, this line of argumentation is bizarre to me.

I don't really care what problems people have with adding packages to their cabal files (as a CLC member, at least). This is part of doing Haskell.

Whether to add a function/symbol to base should have a higher bar than "I'm too lazy to import a package". Now, todo definitely may qualify. Other stdlibs have it (the rust macro was mentioned).

However, in all this discussion I'm still missing a good argument why we can't just add a warning to undefined and call it a day. "Werror users will be sad" is not a good argument to me. That would mean we can never introduce deprecations or new warnings. That's not how we operate, I believe.

hasufell commented 5 months ago

I'd also be interested in the opinion of @chshersh

googleson78 commented 5 months ago

However, in all this discussion I'm still missing a good argument why we can't just add a warning to undefined and call it a day.

I believe https://github.com/haskell/core-libraries-committee/issues/260#issuecomment-2016332241 is a somewhat good reason to not do this.

Kleidukos commented 5 months ago

But those warnings are entirely suppressible, no? Like disabling orphan warnings in modules that contain orphan instances.

ekmett commented 5 months ago

I do think a warning on undefined is an outright terrible idea.

It is a combinator that has a number of legitimate uses. I mean it says it right on the tin, you may well have to use it when the answer is undefined. I say this as someone who is not a member of the Werror crowd but likes to have his code build clean because its used by so many users.

There's tons of base packages that use it as a form of detailed strictness annotation. Witness all the macros in libraries for stuff like bytestring that do things like:

foo a b c | a `seq` b `seq` False -> undefined

before proceeding to all the actual patterns to avoid encoding necessary strictness as a side-effect of the first pattern, and/or to support ancient platforms that might not have bang patterns.

I have several hundred uses of it (modulo what I'm sure are a few false positives) in my codebase.

... and the only way to suppress the warnings it would pick up are insufficiently granular and would mask much more legitimate warnings.

I have little objection to adding TODO to a module in base as I think it is quite clever and crosses a non-triviality threshold with the warning, interactions with callstack, interactions with polykinds and runtime representations. Especially once the pattern is introduced. That sort of thing is pretty tightly coupled with the compiler and isn't what we should expect the typical Haskeller to be capable of fully rolling on their own.

I personally do think it fails to rise to the Prelude bar, though. Especially not in one fell swoop. If we add it to base and in a few years it is seeing widespread community adoption, by all means we can consider re-exporting it from Prelude, but this rarely turns out to be the case, as adoption usually lags early enthusiasm, and practices take a long time to change.

There's lots of useful stuff we could pack into Prelude. We import minimal subsets of a lot of libraries for historical reasons, and taking fresh names, especially useful common-looking ones, there has to be a very carefully considered thing as they break the code of anybody else who took that same name unless they explicitly exclude that import.

Haskell's semantics could have been defined such that local names trump imported names, and this would mitigate this to some extent, (though you'd still run into it the moment someone exported that symbol and used it in another module), but it wasn't.

And no, I don't think requiring everyone to import Prelude qualified or with explicit usage masks as some vague future-proofing technique is an acceptable alternative. The rate of these incidents is quite low and the maintenance burden of such explicit imports is quite high.

ekmett commented 5 months ago

I believe #260 (comment) is a somewhat good reason to not do this.

Those are, and I'm also firmly in the "no warning on undefined" camp, but I do feel I'd be remiss to observe that this particular case would have been better handled via the Proxy trick had it existed at the time those libraries were written.

An interesting exercise for the committee would be to find a way to migrate siblings of those methods to Proxy and/or explicit type arguments with an eye towards the eventual deprecation of the originals.

MangoIV commented 5 months ago

If not Prelude, which module would be best suited for this?

Debug.Trace is imported very often for similar (only temporarily for debugging) purposes, so I would propose that one, else, but perhaps less probable, I would even prefer to have a Debug module… (less to type, less misleading)

MangoIV commented 5 months ago

I can refine the proposal accordingly if people think that’s a good idea.

ekmett commented 5 months ago

While I think the module name is a bold choice, I'd be quite fine with Debug. It is clear that you are doing something temporary.

And to a lesser extent with Debug.Todo (which I think would be confusing with the pattern being named TODO) or even something in GHC.Debug if push came to shove.

I kind of think Debug.Trace has some connotations to it that are lacking here. This isn't about tracing execution and carrying on. That module is basically all about printf-style debugging. This ain't that.

The main question I'd have is if something like unimplemented should go in the same place (sans warning) to cleave apart the idea of something that the author intends to be implemented later and deserves a warning from the stuff that is merely not implemented because you are forcibly implementing a typeclass that doesn't quite fit your semantics or whatever.

I have one more question, which I didn't see addressed above. I realize the primary intended use of these is progressive implementation, but I try not to be TOO prescriptive in language design. Should this use a raise# of an errorCallWithException like it does now or should it use a typed exception? Error calls definitely get handled if someone is handling "error", but if we ever want someone to be able to distinguish these without checking the exact text of the message in a handler, then a typed exception with enclosed callstack might be a better fit.

Also if you are going to use raise# for this, it starts to makes sense to include todoIO to ensure that it doesn't get lifted into the wrong place when used to replace an IO action, like with how we have both throw and throwIO. Note that this is definitely pushing in the direction of it being in a module of its own.

MangoIV commented 5 months ago

Thank you for the remarks, I will consider the typed exceptions part but I don’t know if the other proposals (adding even more things) are in scope for this proposal:

As I see that including in Prelude seems unlikely at this point in time as many CLC members and also users object, I will adjust the proposal as follows:

If CLC members so wish, I can also make a separate proposal for the Debug module itself.

Kleidukos commented 5 months ago

@ekmett oh I could see interesting tooling that would aggregate and report on UnimplementedExceptions. Nice idea!

mixphix commented 5 months ago

Exporting it from a module named Debug.Todo on its own is exactly what the todo package already does. You can already write import Debug.Todo at the top of your module and, if you have chosen to depend on the todo package, all of its apparent glory and wonder is available. It does not need to be exported from base.

MangoIV commented 5 months ago

I think it is unhelpful to ignore a large part of the arguments made if we want to come to a good conclusion. :/

mixphix commented 5 months ago

Let's invite the maintainer of todo, @nyson, to the discussion.

mixphix commented 5 months ago

ignore a large part of the arguments made

Don't piss me off.

ekmett commented 5 months ago

I likeDebug.Placeholder and top level Debug re-export of that and Debug.Trace. If you are going to put together the module I'd include the todoIO and unimplemented and unimplementedIO bits there, no point leaving the job half done. I admit I'm just one voice, and I'm not even on the CLC these days, so take my input for what it is worth. If we make these throw proper exception types, then including the exception types themselves in here also makes sense.

It seems worth noting that this is a different API than @nyson's Debug.Todo for better or worse. More general (works at more type reps), but less control over error messages, and doesn't include the trace machinery in the placeholder module, though does in the Debug top level export.

Also the suggestion of a pattern synonym I think would flesh things out well into non-trivial territory.

I'm very sympathetic to wanting an easy at hand tool for debugging purposes. One reason why I favor base over a separate package is quite banal: I do switch in and out of using these sorts of tools rapidly, and stuff that forces me to change a cabal file over and over and over as these come in and out of use means that tools like ghcid break every time I change the package dependency set. That sort of rapid oscillation isn't really the case with something like, say, filepath, etc. where the addition to a package's dependencies tends to be quite monotone.

A workflow where I can be chugging along, decide I'm not ready for something and just casually tack in an 'import Debug' and put TODO cases in my code that then get tracked and reported during builds rather than yelled about purely at runtime seems desirable, and I can know I've well and truly cleaned up the code when the import Debug statements are gone as a sanity check.

mixphix commented 5 months ago

A typed hole works just as well, is more common and established in Haskell codebases, provides more information, and prevents accidental compilation of incomplete code. What's the problem?

tomjaguarpaw commented 5 months ago

For the record, my current favourite implementation is @ekmett's at https://github.com/haskell/core-libraries-committee/issues/260#issuecomment-2016344264. I don't know what module it should go in, but I don't understand why it should be in the Debug hierarchy. What does it have to do with debugging?

ekmett commented 5 months ago

A typed hole works just as well, is more common and established in Haskell codebases, provides more information, and prevents accidental compilation of incomplete code. What's the problem?

I want to be able to run the code that I do have complete. A typed hole doesn't let me do that without using a much more drastic flag like delaying everything to runtime type errors and that sweeps everything under the rug, so it isn't suitable for purpose.

ekmett commented 5 months ago

I don't know what module it should go in, but I don't understand why it should be in the Debug hierarchy. What does it have to do with debugging?

Fair point. I think we were perhaps primed by the existing package which lives in Debug.Todo.

Perhaps Control.Placeholder or Control.Todo or Control.Unimplemented would fit better with the current hierarchical namespace standard. Even if it is slightly long, nobody could really complain about taking a name like that.

On the other hand, on reflection I'm somewhat negative on the top level Debug module, because there's a lot of folks who develop in haskell for applications with top level namespaces, and a Debug module is a common end-user type name, and they'd have to do something awful like use package imports to hack around GHC stealing their module name from them.

ekmett commented 5 months ago

One negligible advantage of Control.Todo is a "grep -r -i todo src" would find those and your todo-list items in one go, and is basically a typical sort of scan folks use to find todos today. The downside is that it'd find imports of that module for unimplemented's too, which pushes me towards Control.Placeholder.

mixphix commented 5 months ago

Once again, base is not the place for an unstable or experimental design. Vacillating between pattern synonyms and a top-level declaration only leads me to believe that this proposal has not been well thought out and is not suitable for the standard library. Once evidence has been gathered for an interface that is both stable and in common use, I may reconsider my position on this issue. Until then, continuing this discussion when there already exist multiple ways of obtaining this functionality that have been repeated several times over in this thread does nothing but to increase my desire not to effectuate this change to base.

ekmett commented 5 months ago

Once again, base is not the place for an unstable or experimental design. Vacillating between pattern synonyms and a top-level declaration only leads me to believe that this proposal has not been well thought out and is not suitable for the standard library.

The pattern synonym is an augmentation to a solid base proposal. I'd live without it, but I do think this is the right time to bring up such wibbles and to steelman the proposal to be the best it can be.

The existence of such wibbles and talking them through in excruciating detail is the very thing the core libraries committee was created to do. If every discussion that ever made it here went through without discussion and without variants being proposed and discarded, I'd view that as frankly a broken and flawed process. This is precisely the time and place to sharpen proposals. I'd rather see more proposals than fewer, and that means that some of them are going to be less than fully baked. I don't think we should merge everybody's first idea into base, but that doesn't mean they can't be the seed of the right idea.

I also accept the fact that I don't always get what I want, democratic sausage gets made and the process is unsightly.

jeukshi commented 5 months ago

Once evidence has been gathered for an interface that is both stable and in common use

Both undefined and error are stable and in common use, so evidence is already here (does not apply to pattern synonym, but it might be strictly better, I don't know).

Both of your proposed alternatives completely miss the point; holes introduce compile errors, while todo removes it, a library is not a match to built-in functionality for "quick and dirty" programming technique. Doing nothing and sticking to error "TODO" is a better alternative than those two (and, IMO, an acceptable one).

I offer another improvement to the proposal: let's mark todo as deprecated, with the message that it is deprecated and can be removed at any point in the future. That way, no promise of any kind of stability is being made.

ekmett commented 5 months ago

I've taken the liberty of throwing something together over in ekmett/placeholder with all the suggestions above taken in. (You may have seen a notification cross-post to here a half-hour ago.)

If somebody wants to hack and slash on that to make a bunch of examples and tests, and/or even co-maintain this on hackage I'm all about that. Reach out.

If this proposal goes through, then maybe that can serve as a robustly worked candidate.

If not, then maybe it can serve as @mixphix's "do this outside of base" Schelling Point.

MorrowM commented 5 months ago

ignore a large part of the arguments made

Don't piss me off.

@mixphix Respectfully, this is straying from the HF Guidelines For Respectful Communication. While they're merely guidelines, I'd like to think that the CLC could be held to such a standard.

hasufell commented 5 months ago

I do think a warning on undefined is an outright terrible idea.

It is a combinator that has a number of legitimate uses.

This argument has been laid out multiple times and yet I've not seen an explanation why we should treat undefined any different from head and tail, which have even more legitimate uses and are throwing warnings now.

MangoIV commented 5 months ago

Thats a bit of an imprecise argument but I think that

(I personally would also not have proceeded by adding warnings to head and tail, tbh)

hasufell commented 5 months ago

The only argument I can see is that:

But then: do all warnings have to be actionable, in the sense that: "I can rewrite my code in a semantic preserving way to get rid of the warning"?

silky commented 5 months ago

do all warnings have to be actionable, in the sense that: "I can rewrite my code in a semantic preserving way to get rid of the warning"?

Definitely.

MangoIV commented 5 months ago

But then: do all warnings have to be actionable, in the sense that: "I can rewrite my code in a semantic preserving way to get rid of the warning"?

I would say the answer should be yes, but independent of that, I think more importantly, I would expect the scale to be different. I e very, very few legitimate uses of head and tail and a lot of legitimate uses of undefined. This is again very anecdotal, I don’t have any numbers on that.

jeukshi commented 5 months ago

I have an idea for an alternative to this proposal: do what Haskell does best... abuse do notation.

foo :: Bool -> Int
foo = \cases
  True -> do
      let bar = "something"
      do -- do it later
  False -> do -- same

This doesn't work right now (and won't ever with NondecreasingIndentation) as it is an error (Empty 'do' block), but if GHC had a flag to defer it... Bonus points: it is simpler to type than todo :)

ekmett commented 5 months ago

Just as a joke to steelman @jeukshi's comment...

ghci> :set -XBlockArguments
ghci> let to = error
ghci> to do "something"
*** Exception: something
CallStack (from HasCallStack):
  error, called at <interactive>:4:10 in interactive:Ghci2
MangoIV commented 5 months ago

I have an idea for an alternative to this proposal: do what Haskell does best... abuse do notation.

For the matter of this proposal, I would like to keep it as simple and obvious as possible, while I think overloading syntax per se is a nice idea for certain usecases, I don't think it's appropriate for this usecase, at all.

MangoIV commented 5 months ago

after this long and, I think, very fruitful discussion, I have amended this proposal with several small improvements and adjustments s.t. the CLC is more likely to accept it and the ecosystem will benefit even more from it.

tomjaguarpaw commented 5 months ago

Thanks @MangoIV, I like the amendments, except I still don't see why this belongs in the Debug hierarchy. I'm not sure what more I can add about that than it's simply not used for debugging. I don't find the claim that '"writing code" is closest to "debugging"' convincing at all. I don't have a particularly good alternative though.

I think the proposal and implementation are excellent. Normally I am very opposed to adding anything to base that can be defined outside it but the case of todo is a special situation. It's a good argument that pulling in a separate package for it is a heavyweight operation. I sympathise with both sides of the matter.

MangoIV commented 5 months ago

I don't have a particularly good alternative though.

I also don't find my arguments very convincing, I'm still searching for a third alternative; I personally find Control even worse though.

tbidne commented 5 months ago

todo feels like a dev tool to me, so if a new top-level name isn't so bad, I could imagine something like Develop.Todo. I could not find any Develop.* modules using serokell's search, but note that there is some prior art for Development e.g. shake, gitrev.

jeukshi commented 5 months ago

I don't think it's appropriate for this usecase, at all.

I disagree, although I don't think it is worth discussing right now.

I just want to state that, I like this proposal, and I like it better than any alternative posted here. I don't know if it deserves to be accepted, but I, for sure, would love the future where I can replace my (bad?) habit of spamming undefined with spamming todo.