Live CEOing Ep 739: Language Design in the Wolfram Language [ErrorHandling Paclet and Message]

Connor presents an experimental error-handling framework for the Wolfram Language built on Failure objects. The core API includes raise to throw failures and handle to catch them with pattern matching. The team discusses ergonomics, stack traces, and whether this non-lexical approach should eventually become part of the system.
Chapters
Sam.
Okay. Hello, everyone. Welcome to another Wolfram language design review, part of our incremental language development series. And today, Connor is sharing his screen and we are talking about error handling. And Connor, take it away.
Okay, well, right off the bat, I'll just warn everyone I've got a bit of a cough, so apologies for that if that shows up.
There's no bug in the code.
There's a bug in the human, though, unfortunately, yes.
Okay. We're talking about machinery for debugging code too, so sorry, it doesn't work on humans yet. Okay.
I'll be the first to sign up when it does. Yeah. Right. So what we're talking about today is an effort we've been working on for a while at this point, which is we're trying to design essentially a set of an error handling framework for Wolfram language that synthesizes what many Wolfram language developers at the company and more broadly have learned about common patterns of error handling in Wolfram language in kind of large Wolfram language programs. So we've had a design effort underway for a while to kind of synthesize that experience. And what we have now is we think, you know, we're nearing kind of the first version of this that we think is ready for experimental usage, essentially. And what we're really keen on is we think that there's. That what we really need here is real world experience with this functionality. You know, we think we're starting from a good place because we've already learned from the experiences of many Wolfram language developers and what they've done in their projects. But we really want to validate this design and make sure that it's rock solid before we proceed through our normal kind of standardization and systemization procedure.
Hold on a second. What size of code is this relevant for? Like, if I'm writing a WFR, for example, that is 10 lines long, would I care about this or not?
I think you could use this functionality for that. It would be appropriate. And certainly it depends a bit on your style. I would certainly use this functionality in small programs. Other people might feel differently, but it's primarily intended for projects that are certainly on the order of hundreds to thousands, tens of thousands of lines. That's what it's kind of geared towards.
So give me a sense here. I mean, we've got the Confirm and Close stuff. Is this intended for larger programs than Confirm and Close? Is this. How does this relate to Confirm and Close?
Ultimately, this will ideally integrate well with Confirm and Close, but it's maybe a superset of what Confirm and Close is trying to do. And it's kind of fundamental mode of operation is a little different. Confirm and Close is unique in that it's a piece of lexic. You know, it primarily does lexical error handling. And what we're working through here is something based around essentially exception throwing and catching, which. Which is non lexical in the general case.
Okay, okay. But so confirming close might be a more lightweight thing. And so my next question is this kind of apparatus, do we imagine like that, you know, an LLM might suggest to people, you know, given a large piece of code that we already have, do we expect that one day we're going to go through and look at existing code we have and say, and have perhaps an LLM suggest, add these kinds of annotations to it? That kind of the idea?
I think that would certainly be a reasonable thing to try. Yeah. And we've done some preliminary tests asking LLMs for very short pieces of code, and the results seem promising, not perfect, but they perhaps provide a good place to start.
Okay, all right, jump in. How does this work?
Okay, so I gave the bit of background about motivation. Let me talk about the actual how the framework is structured. So the basic idea is we're trying to invent as little new as possible in terms of representation of errors. So we've had, for the last few years, I don't know the exact introduction date, but we've had this. This notion of failure objects in Wolfram language which have been used in newer parts of the system. So we're building this new system. Its core is this failure object representation. We do not have our own representation of errors, but we do extend failure by convention with a couple of new. Well, new things. But I'll talk about those in a second. The second point is, like I mentioned, it's very much geared around the kind of. The primary functions here are raise and handle, which are analogous to throw and catch, as the note here says. But they assume that the payload that is being thrown is a failure object. So they're designed to make that a very ergonomic. With a short piece of code, you can throw a failure, you can catch a failure, you can conditionally catch a failure using pattern matching, that kind of thing. Oh, no, sorry, Siri, wasn't talking to you. So raise and handle are really the core things we talk about here. Now, this packaged error tools, like I mentioned, raise and handle are the core things, but there's a number of other functions in here that are much more experimental. They're kind of Trying out ideas for what might be useful, say utility functions for error handling. So things like assertions that throw instead of issue messages, or a version of replace that, that issues a failure if there's not a rule to handle the value that was being replaced.
Look, okay, that goes into one of my pet issues. There are gazillion functions which in a sense could have messages off by default, like replaces. A typical example of a thousand such situations where like, oh, it didn't replace anything. Well, that could be exactly what you wanted, or it could be an error condition. And if we only had a mechanism for having messages off by default, you can go look up essentially for a given function. Oh, these are things that I might worry about. Let me handle those things. That would be a reasonable way to do it, in my view, rather than peppering kind of special versions of functions all over the system because there's so many functions. I mean, it's like, like I'm trying to solve an equation. The equation has no solutions. Okay, that might be exactly what I want. I might have a giant table of equations and some have solutions and some don't. Or it might be an error condition.
Yeah, and I tend to agree. I think it matters a lot on what specific function you're talking about. I think for something like the notion of assertion having that be a globally settable property is much more viable. But for something like this, you may very well have in the same function the same block of code, one replace is allowed to fall through, and that's perfectly valid. But another replace kind of means something different.
I agree, but I'm saying if we had a confirm quiet type mechanism that could fish out messages that are off by default. In other words, it is looking for the, you know, no replace message from replace, which you would never normally see. Like, you know, without a confirmed quiet of no replace, it's just like, it just goes right by. It's, it's takes no time and it does nothing. Right. Yeah, but this is a way of, of fishing out things that are fishy, so to speak. You know, by specifically calling them out. I, I really believe in this mechanism because I think that there's just going to be so many cases where it's like, well, that could be a mistake. It could be not a mistake. You know, I don't know. It's, it's kind of a, a, you know, a file that turns out to be zero bytes in size. Maybe it's an error, maybe it's not. But if we had a sort of a, a message off by default, that says the file is zero bytes. Then you know, you could, you could basically trigger on that if you wanted to.
Yeah.
Anyway, my 2 cents worth on that. But that may not be relevant to a lot of what you want to talk about.
Right, Well, I think, yeah, that's not real. I mean, that is, it's relevant in one sense, but there's a orthogonal part of this framework that's not far enough along that we've, we've documented yet. But that is that, that very closely mirrors that, that notion you were just talking about of kind of off by default messages. But it brings some structure to them. Kind of like what we're doing with, with raise and handle. So that's something we've been exploring, but it's not yet at the point where we feel comfortable with the design even to propose experimental usage. Okay, yeah, all right, so let me, I can get into the example. So just basic raise, like I said, this is all built around failures. So the basic idea is what raise does, it assembles a failure object and it throws it. So its basic argument structure is very similar to failure in that it takes a tag and an association of kind of metadata. It additionally has a kind of convenient syntax for specifying a message along with that failure. So a format string and some arguments essentially. And I'll have an example of that.
A small detail, but please let's not use the, you know, the camel case convention for an actual usage message. Ah, yeah, we don't tend to do that. Okay, onward.
Yeah, that should probably be like fmt stir or something. Or just fmt. Yeah, I'll fix that. Handle is like in this analogy, raise is to throw. What handle is to catch. So handle takes an expression that may or may not throw a raised exception and it optionally, sorry. And it takes a second argument that is a pattern that describes what kinds of raised failures you want to catch. And the intention here is why don't I just show an example? Actually, before I wax on, let me.
Make a comment though, that, you know, throws tags are one of its least. You know, we don't like those. They have messy stoping, et cetera, et cetera, et cetera. A lot of confirming clothes. And its lexical approach was born out of trying to avoid the horrors of tag management.
Yeah, so. Well, one of the things that we kind of. The conclusions we came to in our discussions, and reasonable people may disagree about this, was that one of the difficulties in practice of using tag, throw and catch is that there's often, I mean, first of all, Just having to manage that is somewhat cumbersome. It's a tax kind of. But it doesn't really afford you the ability to do things like conditionally catch where you may want to catch a raised failure if it matches a certain pattern, but not otherwise. And that's really about the payload, the thing that is thrown, not about where it was thrown from. So we support pattern matching. Yeah, yeah. So let me, let me show an example, but I can get back to that topic. So, basic idea. This is probably not going to be a required thing, but for now we do. You have to register a tag that you're going to use with raise and handle. The intention here is that this is something that would allow you to construct error hierarchies, essentially. So you have a tag that is somewhat. It's kind of the general error that denotes any failure in a certain category and it may optionally inherit from other failures. So, for example, you may have a general class of input output errors, IO errors, and you may have a more specific error type, say file does not exist error that is also an IO error. And that came out of the experience of developers who've worked on projects with, with extremely complicated error handling, where you often want to be able to model more granular errors and then handle those based on, you know, conditionally at some higher level, saying, here's. Here's how I handle general IO errors, here's how I handle general database errors, and so on. So this is, you know, the intention here is that this system can kind of. It has a very simple initial usage, but can become more featureful if you want to opt into, say, this more advanced representation of error tags. I don't have an example of that here, but I'm just calling that out as something you could do. So basically, yeah, register a tag and then raise an exception. Now, I haven't handled this exception, so this is just an uncaught throw. But you can see that what was thrown was a failure that was assembled with the tag that I specified and the message that I specified. If I want to handle failures easy enough to just handle every failure, don't discriminate, that's possible to do. Alternatively, you can specify pattern matching rules that they're just patterns, they match on the payload of the thing that was thrown. So in this case I'm going to handle foo errors. But let's say it was a bar error instead that was raised. Now, this passes through the handle, either being an uncaught exception or something that is handled at a higher level by a Different handler. So that's the basic idea of the behavior of raise and handle.
Yeah, hold on, hold on, hold on, hold on. So these tags. Okay, let's say there's a. I mean, I just don't understand how you manage the tags because this is dynamically scoped. You might have a giant, you know, code block of somebody else's packlet that you are calling. And your, your point is, unless they have an uncaught, you know, an exception with an uncaught tag unquote in their machinery, you'll never see it. Yeah. That's your claim.
Yeah. And the convention we're going to encourage is that top level functions, public functions that you'd expect to actually be called, will handle their exceptions. They should not have uncaught exceptions.
I certainly hope not. Right, so, okay, so then. Okay. And presumably we will have, we could have. How much, how easy is it to do the analysis of whether there could be an unquote exception? It's not too difficult. I mean, assuming people don't, you know, assemble code on the stack. More or less.
Yeah. I mean, and the convention we're encouraging is that what you would do is if you have a top level function, you know, here represented just by a function that's, that's capital camel case, you would tend to write something along these lines where you, you're using the operator form of handle that's wrapped around the body of your function and consequently encompasses both that function and all other kind of internal functions in your package. So nothing could really get past this.
So that's a sub value form of handle, A one argument form of handle, which is. Which, which is an operator form, so to speak. That is equivalent to handle of get tuples comma blank failure.
Correct. Yeah. And it's, it's specifically designed to be very ergonomic in this use case where you want to very clearly see when you're.
I mean, because we have nothing like that with throw. That's a, that's a good reason for handle to even exist is the operator fault.
Yeah.
So does handle not hold anything? How, how can this work?
Handle handles returns pure function, which is holding. That's the only way to make it work currently.
Yeah. If you do. I mean, this is. Well, yeah, but it's, it's just returning a function with attribute hold first.
I see. Well, that's kind of ugly. That's something we'd want to fix.
I don't disagree.
Yeah, we're going to need, one day, we're going to need sub values able to Deal with attributes. Yeah, we're going to need a, you know, that's an evaluated change. Okay.
Yeah, so that's, that's the basic example and I kind of can talk through this example. I won't go into too much detail, but this is how it's imagined. You would use this in general, I've written it this way with this kind of body function being separated from the definition just so that I can explain this alternative. But I'll get back to this in a second. But the general idea is. Okay, so what does this function do? Well, it reads a file. So, okay, it calls out to a function called readfile, which itself can raise failures. So if you have a look at the definition of readfile. Yeah, it checks things, says hey, file doesn't exist. It attaches any relevant attributes as metadata and the association along with the descriptive text does its thing. Now crucially you'll notice that readfile does not handle its exceptions. It does not have that handle underscore failure. Yeah, so that means. And this is where we would compare to confirm and close. This means that we don't have to. If this was a confirm and close based project, you would have an enclose here and some code that kind of throws the failure or you know, returns the failure here. And at this call site you would again, you'd need another copy of confirm confirming the return value of this function. So using this non lexical raise and handle approach, we don't need to have intermediate confirms, just propagating errors up the stack which in a large project it's kind of a lot of line noise where you just. Yeah, so that's the idea. Anyway, this function goes through whatever splits the contents. It does some validation of the entries in the file. So it has this internal error entry thing gives you some contextual information about. Hey, it's this line on which the error in your file appears does its thing. So let's see what actually calling this function.
Hold on, hold on, hold on, Sorry. What are the arguments to raise? So the arguments to handle. Where's the one argument form handle?
That's here.
Okay. And raise raises that exception. So there's no. I see. It's a pure side effecty thing. What does raise return?
It doesn't. Because it, it. Its control flow diverges. It's, it's like throw. It doesn't return anything.
I see. So whenever you hit a raise, you're propagating outwards like throw.
Correct.
Okay.
It helps just to. What is the definition of this? You can See, it's, it's definition at least in the common cases is pretty simple. It's just a throw of this other helper.
Okay.
Okay. So just getting back to the example because it shows off a couple of piece fun that functionality pieces. Let me make sure actually that I.
I mean technically throw returns null but you'll, you'll never actually see that.
Right? Yeah. So we've got things in the kernel working correctly but yeah. So let's, let's look at a little example. Let's. Let's.
As opposed to from early pre version one code the I don't know whether that's still around the $$ luxury yacht as the return thing when didn't know what to return. That was a fine contribution from an early programmer. Okay.
So let's just walk through. So this is a. I've just made a file that's valid. So get tuples works as we expect. Let's have a look. If we give it you know does not exist Txt we get our error file doesn't exist tag useful metadata about. Well what. What was the object?
Can I see what. Wait a minute. File does not exist. That was a file path that was given as a sock. And so the first thing is a tag in the rays and that's a tag you registered somehow, although you didn't really need to do that.
Yeah and we'll probably remove that requirement ultimately because in the common case you don't.
I mean it's kind of like typing as in it's, you know, it. The. As you add bureaucracy. You confirm the programmer was paying attention and didn't make a typo, for example.
Yeah, there are arguments for keeping it certainly.
Well, I mean the thing is there's partly an IDE UX type question of whether you know, like for example, you know, we could perfectly well in the code assistance system tag whether, you know, like let's say there was a typo in somebody's tag. The question is can we readily tag that so to speak. We readily indicate that. And if the answer is yes, I don't think it's terribly important to have these things where you're sort of forced to sort of enroll in the bureaucracy. Okay.
Yeah, I think there's a good chance and I really, I don't want to open this can of worms because this gets complicated very fast. But some people here may have noticed that one of the disadvantages of the way this is structured is if it encourages you to write these raw strings in here, that makes localization much more difficult. Right. So one of the things that. And I have a note about this, this hasn't been documented, but you can just do right now, there's no reason you can't write this. And this is, you know, if you have a system where, you know, this encode.
No.
That evaluates to the string right now. Yes, not a desirable feature. But it is. But I mean, we could, we have to intercept that evaluation and because we. We had a convention back from version one of, you know, symbol, colon, colon, name, message, name, colon, colon, language. Right.
Yeah. Which.
Yeah. Which I assume we use in actuality.
Yep.
Yeah.
But I don't understand how this would work unless. Unless raise is holding that second argument.
Well, the idea is that this, I mean, this is not work through, which is why I'm saying I'm kind of wanting to keep the lid on this can of worms whether this would issue a message or not. Because if it doesn't need to issue a message, and you're essentially purely just using this for the localization or message lookup functionality, you don't really care about keeping it held. You just want the string contents of the message. If you do want this to issue a message as well, that's a different scenario that we'd have to work through whether that's actually reasonable behavior for raise. Okay, I just want to point out as a stopgap measure, using our existing localization system, which may be perfectly reasonable, this works. But we do tentatively have thinking around if you want to be able to write a, you know, a readable error message here. We would very much want to have linting to check that this string exists in your localization lookup, you know, key value database where however that ends up being implemented, this is something that's not. We've not worked through at all, but.
Well, I mean, the other possibility is. Yeah, you see. Yeah, we, we want to be able to. Gosh. I mean, that seems like a tool that needs to be able to go through a piece of code. Okay, here's a way to think about this. You write a piece of code, you interactively write it. It's very fast. You don't have a lot of error handling stuff in it because you're just trying to see whether it basically works. Then you're going back and robustifying the code. Then you might be going back and trying to do things like make the code localizable. So if we provided the right tooling and the tooling actually worked to go through and say, let me take my quickly written piece of code and let me Go and make it more robust and give me suggestions for how to do that. Oh, you put an arrays, you put in a handle, you do this, you do that. That to me would be a good workflow because that's what a lot of people, I mean, some people know they're writing code for the ages right from the outset. Okay. But many times in many usage patterns, you initially are just trying to explore something and get something to work. And then you might come back and say, okay, now I want to make it into a wfr. Now I want to make it into a more robust thing that I'm going to use for the future. And then you want to put in error handling and localization and so on as a post hoc kind of thing as opposed to, I'm doing this from the beginning, I'm going to sign up for the giant bureaucracy from the start.
Yeah.
So I, I don't know is this, is this plan? I mean, have we thought at all about what that looks like from a UX point of view to take a piece of code and say, you know, I mean, it probably is a toolbar type thing and say scan this notebook, for example, for things where it's probably going to be beyond the notebook. It's going to be a full project and idea, you know, scan this for places where one might add error handling capability.
We've not done much more than kind of just very general thinking about do we think this would be something that, for example, an LLM would have a relatively easy time writing? And we have done tests around. If you give an LLM, say the usage messages of each of these functions and then ask it, write a program. And I've done this. Write a program that does essentially some basic file processing using these error handling functionality. It makes reasonable suggestions. It wasn't perfect, but, but it, you know, it got, it got like a useful degree of the way they're like, ah, I see now, okay, so that's a possible error condition I should handle and the human can kind of fix up whatever minor mistake the LLM made, whether, you know, the overall workflow, exactly what level of suggestions the LM should be making, you know, what, what, what the appropriate moment to invoke the tooling is. That's not something that I, I think I have a clear idea about what the UX should be yet.
Okay, but, but, but to make a different point, I mean one of the things about error handling, so I mean the overarching feature of error handling is there's a, a main code path for a piece of code which the programmer thought about. Then there are all the things that might go wrong, most of which the programmer didn't think about. And to me, a lot of error handling is how much can we automate the secondary code path of the thing that the programmer didn't think about, so to speak.
Yeah.
And, and also to what extent do we want, you know, as we've talked about before, there are errors where.
You.
Know, you're doing exploratory data science and something goes wrong and some piece of data is absent or whatever else, and it's going to spray some garbage in what you're doing, but it's okay, you still can see your way through the garbage. Another case is you're trying to decide whether to, you know, launch a rocket or not or whatever. And you really want it to be the case that if anything went wrong, you flag it as something went wrong, you know, you know, stop. Right. And it's a question of where, how one can tell. Yeah. Which of these. Right. So there are, you know, we've talked about this endlessly over the course of decades of, you know, how to define banks of errors that are critical errors versus, you might like to know, that type errors and so on.
Yeah.
Is this, is that relevant to what we're talking about here or is that an orthogonal issue?
It's relevant to the broader question of error handling. It's less relevant to specifically what we're, I think, bringing to this discussion today, which is again, the, the use case that we're primarily keeping in mind is for developers who we're putting a reasonable amount of trust in to make a good faith attempt to handle errors proactively. You know, they're going to think about the code they're writing and try to handle errors, I. E. Us, you know, us who are working on the language every day and writing kind of professional software in it. So that's why we haven't done a lot of thinking about, for, you know, how do we make this work really well as a broader UX question, error handling in general for.
All right, okay, I understand. Okay, Onward.
Okay, let me see. Where did I. Okay, so I gave the basic example of say file doesn't exist. Now let me show what it looks like. If there's a invalid entry in the file, we get a message like this telling us, hey, line three, you've got a problem.
Yeah, it looks simple and obvious and makes sense. All right, so where do things go? Horrible.
Well, let me, let me have one other example on this theme. So I mentioned I have these two variants of get tuples. This other variant uses this wrap raised function, which I'm not going to explain, I'm just going to show and if I call this again, what wrapraised has done is it's taken that original error and made it just a member of a more general error, which is ideally more useful at the level of context that the user has when they're calling this function, which is, hey, I passed in a file and the error it mentions, hey, there's some problem with my file.
So let me see the second argument of RAP raised.
Yeah. So rep raised is itself an operator form and it's essentially the same argument structure as raise itself, but catches any exception.
It annotates the raise as. As the raise comes through, it annotates it with some other information about the context in which it lives.
Exactly. Yeah, good idea. And I've converted a few projects to using these new functions. This has been I found tremendously useful in practice, the name could clearly be improved. But being able to, at the level of the definition of a function where you've often got really good high level context, actually put that context in errors that are coming from lower levels where it'd be cumbersome to pass that.
I like this idea. I mean we've never had this idea of in transit. A throw in transit gets annotated.
Yeah.
So what is this wrap raised again? I guess I missed something in this discussion.
Let me try and explain it. You have a low level raise. Somewhere deep in your code it knew that the thing that actually happened deep in your code was the list was of length zero. But then what you really want to know is that happened inside this higher level function. What you really want to know is the, the, you know, the walker walked off the cliff, so to speak. But that's not known down at the low level. But you can then put at the level of the, of the walker you can put a wrap raised which says if you see this, I, I would get. I don't know, is it, is it any unhandled in this, in this invocation?
It's any unhandled thing. And that's one of the sticking points of the design of this function, is how to make it also appropriately conditional, whether it should have its own pattern matching logic or not.
But anyway. But the point for it's purposes is as it comes out, it knows that that zero length list or whatever is part of the walker function, which it didn't know down at the low levels. And so it wraps the message that's coming out to say, oh, this is a Walker thing. Does that make sense at. I.
Okay, so it's. It's sort of like our proverbial message reissuing function that we would want. That we want to have.
Well, yes. I didn't know we wanted a message reissuing function.
Yeah, but.
Yes.
I've not heard of it, but it sounds similar.
Yeah, well, there is obviously a difference because messages are. They do not divert the control flow. And here we are dealing with the diverted control flow through exceptions, so.
Yeah, but in that sense, not divert the control flow. This is just adding. It's just, you know, the things going by and it's tagging it. It's adding an additional tag to the thing that's racing by being thrown.
Yeah. This one doesn't divert, but it deals with the result of this diversion.
Yes.
Yeah. And if it helps, you can see the definition of this is actually pretty simple. It just. It returns the operator form of handle that catches a failure, uses this wrap failure thing to construct a new failure, and then re raises it. So it's exactly what you might expect. Catch. Change. The name.
Relay is popping into my head.
Relay. Okay.
Maybe it's part of the name.
I'll note that down.
It can have a better name. I mean. Right.
Let's not get derailed by the name. I just had that idea. So maybe jot it down for brainstorming.
Like that. Yeah. Relay. Yeah.
Maybe it's one term. Yeah, exactly.
It's being passed. And as it's being passed, somebody is. Is signing their name on it, which I suspect.
You know, I'm always. I'm always thinking about bas baseball and the relay throw. There's a relay throw, right. You catch one person's throw and you throw it to the next person.
Oh, okay.
Right.
You know. Yeah, okay.
Yeah.
But you don't sign the ball on the way, which is what this is doing.
Right. But you might put different spin on it.
Right. It's like. It's like the game of Telephone or something, except you're intentionally distorting the message. Yeah, well, yeah. Obviously naming of all these is going to be an exercise of its own. This was really the main example I wanted to get through to illustrate kind of how these things are used, the conventions we're trying to form around them. I do have a couple of. There are some other things. There's a lot of functions in here. One, again, that I think is a particular use, again, particularly in these projects where you're trying to be relatively strict in Your code is this setfallthrougherror function. And all this is, is it just. It takes a symbol as an argument and it sets a down value on that symbol, you know, a triple underscore down value that catches anything else. So it's a way to annotate a symbol as being like. This is a symbol that you must call with correct arguments.
Yeah, it's a good idea. I mean that's the thing where there's a lot of silly messy code that tries to deal with that case.
Yeah. And I should be clear, I mean it's a terrible name. Yeah. A lot of these come from like different generations of thinking about the naming convention haven't been cleaned up yet.
I mean, I can't, I don't understand why you wouldn't be able to give a message there, a custom message for what to do in that case.
You definitely should be able to. Yeah, which actually I don't know if I have a note about that, but I'm certainly going to write it down.
Okay. Yeah. You know, I wonder how complicated that function would end up getting. Like. No, I mean it's, it's probably. That is the useful case. It's just if nothing matched, there's a problem.
Yeah. And it should be said. Let me say real quick, it should be said that, you know, the use cases in which fall through error are useful are. It is not something we would ever intend a end user to see. Like, we've done our job, we've got a bug in our code if an end user ever actually sees this. But it does mean that they get a very clear like if they, if they contact, you know, our bug report, email address. We can see very precisely. Ah, that. So somehow somewhere this function was called with incorrect arguments instead of being some kind of huge message spew that fails inelegantly and is harder to digest.
Well, yeah, but I think for wfrs, in most cases you just want to add this period. You know, you can't be bothered to write all the pattern cases and you just stick it at the end.
Yeah. And for what it's worth, in databases and other projects, we have hundreds of these statements sprinkled after pretty much each internal function. The name for this function we use was different, but the idea was exactly what is it? We had something like DB Def Error, something like that. But this was like very specific to.
Okay, so fine, that's specific to that project.
Yeah, but the idea, the general thing was different, was the same. And I have to say that this alone was responsible for catching maybe 80 or 90% of our bugs early. Because typically the bugs that occurred from regressions or elsewhere in internal functions, they immediately became obvious they didn't generate this accumulation of dispute of error messages.
And because it got caught within that one function.
Exactly.
Which is a point there about you asked earlier, Steven. When somebody's quick and dirty writing a piece of functionality and they're not really going to add error handling, if there was any one function that I would definitely use in, even in that initial prototyping stage where I just try to get something working quick on the happy path, it would be this one.
That one would almost be by default added when WFRs are packaged up. Perhaps it almost might be the case that you ask people, you know, there might be some heuristic based on the code. And then you basically ask people, do you want to make sure it only works in these cases? If yes, check here and it'll automatically add that definition.
I think for debugging purposes though, an uncut throw is a very, very bad thing. It's difficult to deal with because you get no stack trace. If there's no surrounding handle that will actually catch one of these, it should probably issue a message at the time that it generates the throw. So that way you can actually get a stack trace from where things went wrong.
Actually, a good point there. Perhaps we should have a variant of this that doesn't throw on fall through. It just makes the function return a failure.
But also we do have stack traces in the works that will be automatically added at the time when raise actually throws an exception.
Can we just add it to throw?
It is basically right.
And the question. Well, I mean, the question is, given Steven's suggestions about WFRs, I mean, do we want WFRs to be throwing uncaught exceptions?
We don't.
Whenever they're called with a wrong number of arguments, which is.
So in that particular case, we want it to error out at the level of that one function rather than. I mean, this is the. This is again this whole question about what's the level of. Of stoppage if something goes wrong? Like for wfr, the typical wfr, if something goes wrong, it's just, well, the function should return unevaluated or with a message. Okay. But it shouldn't stop the world. But there are other cases where we have no choice. You know that the types of sort of fatal errors where you've got to stop everything.
Yeah. And also set fall through predominantly will be used in like 95% of cases. It's useful for internal function, but if we want to have exhaustive definition that catches errors. Also for the end user function, we may have either a different mode of operation for sepulthrough or define another function that basically works for the user facing functions.
What do we want the user facing functions to do? I mean, in this case you want it to raise an exception and exit basically.
Otherwise it could simply return a failure or return unevaluated with a message or whatever mode of operation we see fit for that particular function.
Yeah, fine, but I mean, then what would be useful would be, which I like very much, you know, just like you can call set attributes on a function and specify, you know, set attributes, hold all or whatever else to be able to say. And I think we've talked about this before, but this is a clearer version of this to be able to say about this function. Okay, we got a bunch of definitions. Now add in something which of all definitions fail, the function returns unevaluated. I guess that happens by default, but maybe returns unevaluated with a message. Right? Normally if all the definitions fail, I mean, this is the problem. The problem is sometimes you are a definition fired from a pattern matching point of view. You're deep inside the function, you realize something is horribly wrong and you just want it in fact to return unevaluated. How do we do that?
So like the function has begun, the down value has begun executing.
Right? And now you find out something is horribly wrong as you check the argument structure or something like this.
Typically what people do, we use the local variables computed inside with or something. And then if those values of those variables are not what we like, then there is a conditional down value that is considered not matched by evaluator, even though it already started trying because it.
Was in the slash semi. Right?
Yes.
Yeah, yeah, right.
See, in version point nine, long, long ago, we had a whole collection of mechanisms for this which unfortunately kind of crumbled at the last minute. In other words, there was a mechanism where there was a special value that could be returned that means think of this as not having matched and another value that could be returned that meant, you know, we now know nothing could match this or nothing should be deemed to match. Does that make sense?
I'm curious what the names for those were.
They were dollar. I think they were things like dollar failed actually and. And we can look it up. I'm not sure it's been. That's 36 years ago.
Yeah, but I was just curious. No, no, that's to be inspirational.
They weren't bad names. They weren't bad. But the problem was, and I, I don't remember what crumbled at the end, I think. Go ahead.
Well, I was just going to hazard a guess. I mean, they seem like the kind of thing that, like handling a value like that could almost be poisonous if you're doing even mildly complicated with.
Right. I mean, kind of like our terminated evaluation stuff, which, which we just changed now, you know, so many years later. But I think it's worth looking at this again because I think that that idea of you're deep inside the down value, you find out something is wrong. You want to say return. And now we've got this failure construct. But we have, you know, we. It's like quoting the quote, you'll never win in the end. Whatever it is we return, it's going to be something funky and it's going to be somewhat poisonous. Right. But, but it's just a convention and we have to say if it returns this thing that says, you know, fail now or something. Okay. If you want to return fail now from a function, well, it's not going to work. Well. Yeah, but I think our general experience has been that those quoting the quote situations mostly don't work too badly. Mostly that nothing too horrifying happens in practice.
Yeah. And if it is, if there is a situation where you can't just can't use that magic thing, you can always restructure the code to be a bit, you know, handle it a bit more verbosely in that uncommon situation. Yeah, right.
But I do think that this, you know, return unevaluated if the, look, if, if the goal is return a failure object, it's easier than returning unevaluated because you say return the failure object.
We are actually trying to go to that mode from returning unevaluated for a number of reasons whenever it's possible.
Well, that's an interesting philosophical change.
Say that again.
Well, Linda is saying that return failure rather than returning unevaluated.
Yeah, we are trying to, we are trying to, to do that more because returning unevaluated, as I understand historically, it came from more computer algebra related contexts. But in many cases, when you're returning a failure, you both can give more information about what went wrong. And also it's unambiguously a failure. So you don't have to necessarily accompany it with a message or then try to determine later whether it was a failure or whether it was intended to return unevaluated.
Okay, but look, the counter argument is this. The number one reason for returning unevaluated is the system didn't Know what to do with something. But you might.
Yes, but the thing is that in a number of cases which are more kind of programming in nature than transformational.
Yeah, yes. Yeah, I agree. In the programming cases, failure is better than returning unevaluated. Returning unevaluated. In a transformation, if everything is very symbolic, then returning unevaluated makes more sense. Go ahead.
Absolutely. But also for their handling and for programming cases and the other aspect of that, why it is important often to return a failure is that we want to basically quit as early as possible with an error. So the longer we wait until error is diagnosed, the worse it is in terms of the analysis of what went wrong.
Well, and you might careen off into, you know, burning up all your memory and blowing the computer up.
Exactly.
Okay, well, that's a good argument. I mean that, look, both cases are useful and having a nice clean support for both cases seems worthwhile. Okay, any. What else do we want to cover here, Connor?
Nothing specific, I think that was. We wanted to give you a good sense of general information.
Well, I want to be.
Since you.
Since you've chosen not to be very explicit. I'm going to be very explicit. Right. So this is a packlet that currently lives in the layout, but there is also a version of it in the packlet repository, which is the page on the right that Connor is displaying there. So it's sort of pseudo documented as an experimental thing in the packlet repo, since you expressed some hesitation about this when we presented that same idea to you for the Diff Tools packlet. Although that's something that's going to be at least initially going to be only on the packlet repo, not in the layout. This is something that is both in the layout and on the placard repo, which is.
You want to. You are asking 4.14, should this be documented?
Well, it's. Well, it's. Should it be semi documented in the sense that it's on the packlet repo and people can look. Look there if they want to try this out.
I think we should try to see if we can actually make it documented. Documented as an experimental feature.
Well, Stephen, that may destroy the purpose of this current stage of trying to use it in larger projects because the requirement to trying to document it will require us to do full fledged design review and figure out all things as much as we can to not cause backward incompatibilities later. And at this stage we're simply not ready for that. At least that's our collective feeling.
That's the way it is. So then the question of whether it's in the layout or not seems kind of moot because it's kind of, you know, nobody's even going to know about it except from packlet repository.
Exactly.
So what's your question, itai?
Well, we suggested that, you know, we get Diff Tools into the packlet repo as a. As a, you know, as sort of a staging ground for eventual inclusion, and you expressed a great deal of hesitation about that. And since this is now something that's actually already in the packlet repo and it's also in the layout, are you okay with this?
Okay, look, my hesitation about these things is, you know, the creep of it wasn't design reviewed. It's in the packlet repository, lots of people are using it. Oh, we should just put it in as it is without further design review because it's been there for a long time or because lots of people are using it and it's kind of a design review avoidance strategy. And that I'm not happy with. But so long as we recognize that it's in the packlet repository, people are using it from the packlet repository. The version that goes into the system will be. Will not. May. Will be incompatible.
Yes. Which, you know, I think. I think will also be true of the Diff Tools packlet, because. Right. If nothing else, we will be moving things from the packlet repo context to system when. When that eventually migrates, which hopefully will.
Be not too long for both of these.
Right, right.
Then the question becomes, do you update the packlet to pack it repo to be consistent?
I think you blow it away. I think you basically say you can use this old thing if you want to, but it's legacy and the real thing is the thing in the system.
Yeah, I think you make those choices per case, but a lot of times it would make sense to blow it away.
Right. I mean, you might want to update the packlet. Well, I guess the question is, can you make the. If you could use the packlet repo to. No, no. I guess if you wanted to make this functionality available to older versions, you would have to release it via the packlet server and blow away what's in the packlet repo. Right. If you wanted, like if the DevTools moves into system, but you want to give ex, you know, allow users of previous versions access to that, then you'd blow away what's in the packlet repo and put a version on the packlet server for people to.
You can use the packlet repo back to 12.1, I think so it depends what you create.
But can you create this new system symbols in the packlet repo?
Oh, okay, yeah, I see what you mean.
I didn't understand that. I. To me, if, look, somebody, hopefully we have good enough tooling that somebody, if they use this in their program, their program knows that this is the version 13.3, you know, raise function from the packet repository and if subsequently there is a system raise function, it will just not be the same function. Yeah, I mean, I believe that's how.
Well, which we all agree on. Right. So with the LLM things, right, those sort of started out in the packlet repo and now they're in system, but we're still sort of making that functionality available to previous versions via the packlet repo. And so the question is, you know, what if we wanted to do something like that, but it's going to be a case by case. Sort of. Exactly right. And just like, you know, we make cloud object Updates available for 5 year old versions. Right. So, you know, it's just going to be sort of on the case by case. Exactly how we want to do this. But I think we all agree that, you know, once there's the system version, that's the canonical version that supersedes anything that may or may not have been.
In the packlet repo 100%. Yeah. I mean, we'd slap a giant label. I think we would want to leave it there for anyone who might be using it in older versions.
Yeah, of course it might be an existing code.
Yeah. But we'd slap a giant label on the one in the pack repository. This has been superseded by X, Y and Z functionality.
And we'll do that.
We already do that in the function repository. We have that label ready to go.
Right. Yeah.
And probably put, you know, a max version in the packlet itself so it won't load in any newer version and cause issues.
I don't think we want to do that.
We would.
They should be able to exist simultaneously in case you do have something that's depending on the old context.
Yeah.
Okay, great. Yeah.
Yeah, looks good. This is nice and simple. Well done, Connor and everybody working on this. It looks easy at the end.
Thank you. Yeah, it was certainly a massive team effort and many of the people here to thank for many of the crucial design aspects. So kudos all around.
Looking good. All right, see you all soon. Bye for now.
Thank. You.
More from Guy

Cheryl Strauss Einhorn | The Human Edge: Smarter Decisions in the Age of AI | Talks at Google

Rachel Glennerster | Innovation and the Center for Global Development | Talks at Google

Rebecca Winthrop | Rethinking the Purpose of Education in the Age of AI | Talks at Google

Dr. Robert Wachter | A Giant Leap: How AI Is Transforming Healthcare... | Talks at Google






