Recent

Author Topic: PSA: Don't use FreeAndNil  (Read 7710 times)

BeniBela

  • Hero Member
  • *****
  • Posts: 870
    • homepage
Re: PSA: Don't use FreeAndNil
« Reply #90 on: May 25, 2023, 10:51:10 pm »
The worst about exceptions is all the overhead they produce with fpc_setjmp and fpc_pushexceptaddres, as just discussed on the mailing list, even if they are not used



For example what I have been bringing up all the time is when you have a TCP stream, which gets closed. A stream can be closed while you try to read or write (or any other blocking operation on the stream). A TCP stream closing is not unforseen, as every TCP stream may be closed at any time, but from the perspective of the read or write function it is unexpected (in the sense when you call recv you expect to receive data, and if the stream is closed it is an exceptions to this expected functionality).
So the stream closing is for seen, but when it happens is going to be unexpected. That's why I think an exception there is valid.

That might cause a SIGPIPE on Linux which kills the program without an exception

Warfley

  • Hero Member
  • *****
  • Posts: 1204
Re: PSA: Don't use FreeAndNil
« Reply #91 on: May 25, 2023, 10:53:33 pm »
Who among us knows what all the exceptions are? We don't even know what an exception is! That's why they're called exceptions...

Those that read the documentation know that. If you use StrToInt just go to https://lazarus-ccr.sourceforge.io/docs/rtl/sysutils/strtoint.html and see
Quote
In case of error, an EConvertError is raised.

You don't need to know everything if you know how to look it up. Personally I also don't know what all the functions are, I don't know what all the types in the RTL are, I don't even know what all characters in the UTF-8 charset I use daily are. But that doesn't matter, because I know how to find it out

Warfley

  • Hero Member
  • *****
  • Posts: 1204
Re: PSA: Don't use FreeAndNil
« Reply #92 on: May 25, 2023, 11:05:29 pm »
That might cause a SIGPIPE on Linux which kills the program without an exception
Yeah but you can just set the (I think it's challed) NOSIGNAL flag, and then check errno. A more high level abstraction could then catch that and turn it into an exception.

The performance of exception handling can be an issue, but it happens if you use exceptions or not because it's part of the language base features. What would be interesting is the LLVM backend, which uses LLVM exceptions, which are potentially better optimized for the target platform. On windows LLVM uses SEH afaik, which, if I remember correctly are quite fast, and add very little overhead

SymbolicFrank

  • Hero Member
  • *****
  • Posts: 1251
Re: PSA: Don't use FreeAndNil
« Reply #93 on: May 25, 2023, 11:53:28 pm »
Ok. How about a metaphor / example?

Let's say you have to calculate a route to guide someone from one side of a city to the other one. Of course you will be using a car (US software, like Google Maps), even if you want to use a bicycle. And you will be going in a straight line, unless certain roads are removed from the map.

And, because you are using a car, it might be faster to use the highway that circles the city. The speeds are higher there, and so is the calculated average. The software doesn't care that you cannot do that on a bike.

And how about public transport? Well, it is at most an inconvenience that only the poor use in the US, so don't spend more than the minimal amount of time on taking that into account. It's probably not a valid solution anyway.


So, if you want to go somewhere, you are definitely using a car, and you want to either go straight to the goal, or take the highway that circles the city. Those are your only options.

Do you want to handle obstructions? How are you going to find out about them? Like, it can be market day, or there can be a demonstration. Not a valid blocking issue in the US, so the software won't see that as a failure state, but certainly something that will happen in the EU. But it's inconvenient to have the user flag parts of the traject as "obstructed" (too many edge-cases), so it isn't allowed.


Anyway, you jump on your bike, start Google Maps and tell it you want to go to the other side of town. It will either tell you to go straight and ignore all obstructions, or take the ring road where bikes aren't allowed.

What you actually want (and expect of decent software), is that it takes all those things into account. It will map all the options, try to contact external parties that warn for roadblocks on that trajectory, and advise you what is the best action to take in your case, every time a decision can be made about it.

Like, if you travel by bike, trains are still an option, outside of rush hour. And if you select "public transport only", you don't want it to have "walk the distance" as the only option, because the next bus is due in 55 minutes and you could walk it in 50, if you hurry.


The difference being, that in one case the user has little input except for setting the goals, while deviations are ignored as much as possible, because they're considered fail-states. While in the second case, the software tries to take an interest and actually help you along as good as possible. Step by step.

It's the difference between: "My way or the highway!" and trying to anticipate the best next step. The thing the large language model AIs just became famous for.

440bx

  • Hero Member
  • *****
  • Posts: 3404
Re: PSA: Don't use FreeAndNil
« Reply #94 on: May 25, 2023, 11:57:25 pm »
On windows LLVM uses SEH afaik, which, if I remember correctly are quite fast, and add very little overhead
That "very little overhead" is usually tens of times slower (if not more) than simply testing a return code and, if the exception required a ring transation (which is the common case) those tens of times go up to hundreds of times.

The use of exceptions is, much more often than not, an indication of bad design (often due to laziness... or real programmers don't test return codes ;) )

and let's not forget, in Win32, exceptions are reliant on there _not_ being stack corruption (if there is, the exception mechanism will usually fail).  In Win64, to avoid that problem, there is a separate exception "table" in the PE file which, in some cases, contains hundreds of thousands of entries (chrome_child.dll being a good example.)

exceptions: a grossly abused and misused feature with the encouragement of many "authorities" (such as MS that consistently promotes their use without any exceptions (no pun intended).)





FPC v3.0.4 and Lazarus 1.8.2 on Windows 7 SP1 64bit.

Warfley

  • Hero Member
  • *****
  • Posts: 1204
Re: PSA: Don't use FreeAndNil
« Reply #95 on: May 26, 2023, 01:17:04 am »
The difference being, that in one case the user has little input except for setting the goals, while deviations are ignored as much as possible, because they're considered fail-states. While in the second case, the software tries to take an interest and actually help you along as good as possible. Step by step.

But we aren't talking about any of this high logic level. When talking about exceptions I'm not talking about high level state machines, we are talking about (sub) function level code handling. Your buisnis logic states are on a much higher level and usually a form of emergent behavior of the combination of multiple functions, that reducing it to simple language construct such as exceptions is just plain meaningless.
Pascal with and without exceptions is in both cases turing complete, this means, when it comes to buisnesslogic and implementing a state machine, you can do it with or without exceptions. Your example has absolutely nothing to do about exceptions.

Just to get my point accross, I am just talking about exactly two options to handle some unexpected behavior, result or data within a function:
Code: Pascal  [Select][+][-]
  1. if not TryOperation(Parameters, OutputVar) then
  2. begin
  3.   MyExceptionInfo:= GlobalExceptionVariable;
  4.   case MyExceptionInfo.MyExceptionInfo of
  5.   MyOperationExceptionType1:  // Handle Exception of type 1
  6.   MyOperationExceptionType2:  // Handle Exception of type 2
  7.   MyOperationExceptionType3:  // Handle Exception of type 3
  8.   ...
  9.   end
  10. end;
  11.  
  12. // Versus:
  13. try
  14.   OutputVar := Operation(Parameters);
  15. except
  16.   on E: EMyException1 do // Handle Exception of type 1
  17.   on E: EMyException2 do // Handle Exception of type 2
  18.   on E: EMyException3 do // Handle Exception of type 3
  19.   ...
  20. end;

I'm just talking about the choice between return value/global variable based exception handling, as it is the typical procedural style versus the "new" exception based style. There are of course variants of the first pattern, e.g. you can do pre checks:
Code: Pascal  [Select][+][-]
  1. if not OperationCondition1 then
  2.   // Handle Exception of type 1
  3. if not OperationCondition2 then
  4.   // Handle Exception of type 2
  5. ...
  6. OutputVar := Operation(Parameters);
Sometimes you can encode the Exception directly in the output space:
Code: Pascal  [Select][+][-]
  1. OutputVar := Operation(Parameters);
  2. if OutputVar = SpecialOutputCode1 then // Handle Exception of type 1
  3. else if OutputVar = SpecialOutputCode2 // Handle Exception of type 2
  4. ...
  5. else // use OutputVar

But you must have some way of checking if your function completed as expected, or if an exception happend. And all I'm arguing is that there are situations in which the second option (i.e. exceptions) are preferable to the first (or any of it's variants). And my argument is simple. It is very easy to forgett to do a return value check. As I have shown above in the FCL code, this can happen and stay undetected for decades. But if you are having an exception, it allows for two things, first it will "notify" you when you encounter it in the debugger, making it easier to find than if it just runs silently and destroys data. Second, because the Output of the function is undefined when it was prematurely exited. So if you forget to check the return value for a certain error, your program will just continue but with garbage in it's state.

To give an example, take this very naive StrToInt implementation, something at some point probably most programmers have written at least once:
Code: Pascal  [Select][+][-]
  1. function MyTryStrToInt(const str: String; out i: Integer): Boolean;
  2. var
  3.   c: Char;
  4. begin
  5.   Result := Length(str) > 0;
  6.   i := 0;
  7.   for c in str do
  8.     if c in ['0'..'9'] then
  9.       i := i * 10 + ord(c) - ord('0')
  10.     else
  11.       Exit(False);
  12. end;

This is the extremly classical and simple approach for detecting an exception, of just returning a boolean to signal if the operation was successful. Let's say you use it to convert strings into integers, but you simply forget to check the returned boolean:
Code: Pascal  [Select][+][-]
  1. var
  2.   i: Integer;
  3.   s: String;
  4. begin
  5.   s := '18u7'; // e.g. user just mistyped and hit u when trying to hit 7
  6.   i := 0;
  7.   MyTryStrToInt(s, i);
  8.   WriteLn(i);  // prints out 18
As you can see, it prints out a value, which is the unfinished result of the algorithm, after it has been stopped half way through, so instead of the 187 which the user wanted to type in, its now 18. Any computations based on this result will not yield the results the user expects.


So to actually bring it back to your example with the navigation app. Let's say your navigation app fetches the data for the time it takes over certain routes from a webserver, and the webserver has an issue and sends an incomplete or broken result. You forget to perform validity checks (without exceptions), and instead of going 50km/h with your car, the software now thinks that you can only go 5 km/h per car, but instead that walking will be at 200 km/h.

What will the user do, who just downloaded your app to get to a certain place? Well they will just delete that app immediately and get a better one.
Alternatively assume you use exceptions. You have the same error, and you also don't handle it. So the exception bubbles up. Now when the user tries to start the search, a message shows up, which says: "Error fetching route data from the server" and when the user confirms shuts down. Of course chances are still high that the user will just uninstall and try a new one, but many users understand that network services can be unreliable at times, so some users will instead just try again, and then when the app gets a correct server answer and works as expected, they are happy.

When you ignore an exception (and you have a very basic message and crash mechanism like the LCL provides), the user will be notified about the error and why it crashes. If you ignore a return value, you will spit out completely wrong data, and be useless at best, and harmless at worst.

Do you really think that in this scenario I have outlined, the crash is worse than giving the user a completely unusable result as if it was correct?

Warfley

  • Hero Member
  • *****
  • Posts: 1204
Re: PSA: Don't use FreeAndNil
« Reply #96 on: May 26, 2023, 01:28:38 am »
Just to get my point accross, I am just talking about exactly two options to handle some unexpected behavior

As an honorable mention, I just want to note that there is what I consider to be the best solution, by encoding the error state in the function result, such that you must engage with it, but don't have random jumps down the stack:
Code: Pascal  [Select][+][-]
  1. function MyIntToStr(const str: String): TOptional<Integer>;
  2. var
  3.   a: Integer;
  4.   c: Char;
  5. begin
  6.   Result := None;
  7.   a := 0;
  8.   for c in str do
  9.     if c in ['0'..'9'] then
  10.       a := a * 10 + ord(c) - ord('0')
  11.     else
  12.       Exit;
  13.   Result := a;
  14. end;
  15.  
  16. var
  17.   opt: TOptional<Integer>;
  18. begin
  19.   opt := MyIntToStr('12A4');
  20.   if opt then
  21.     WriteLn(opt.Value)
  22.   else
  23.     WriteLn('not a number');
  24. end.

The problem here is that this a. works only for functions not procedures, and b. only works for functions where the return value is important. If you take the netdb example from above, the error here was that the function result was completely ignored. Therefore encoding exception information in that result would not have helped.

This solution works therefore best for "pure" functions, i.e. functions that have no side effect and are only called to genereate the result value. Because in those cases the result value can be encapsulated in some additional structures (like TOptional here) to make sure that whenever the programmer want to access it, they are aware of the fact that this could also be None.

But because of it's limited availability, I restricted myself to the two most common and generally applicable solutions

alpine

  • Hero Member
  • *****
  • Posts: 738
Re: PSA: Don't use FreeAndNil
« Reply #97 on: May 26, 2023, 12:31:34 pm »
If you don't see the differences between comefrom and exceptions, I expect that you also think that goto is basically the same as a function call.
I can see it. As noted elsewhere, with COMEFROM it is specified from where it comes from. With except it is not.
As for the call - yes, I think so. It is a goto with an additional saving the address of the next instruction, CALL, BSR, BL, etc.
"I'm sorry Dave, I'm afraid I can't do that."
—HAL 9000

Warfley

  • Hero Member
  • *****
  • Posts: 1204
Re: PSA: Don't use FreeAndNil
« Reply #98 on: May 26, 2023, 02:50:18 pm »
I can see it. As noted elsewhere, with COMEFROM it is specified from where it comes from. With except it is not.
As for the call - yes, I think so. It is a goto with an additional saving the address of the next instruction, CALL, BSR, BL, etc.

You seem to not see the forest for the tree. The key is context. Comefrom and goto are arbitrary jumps without any context information, this is chaotic and makes code that uses it hard to understand. Functions and except blocks have a very rigid context definitions, in both cases you introduce a new scope to which the jump is limited. For a function you land in your function context, with your local variables, etc. It is completely independent from where the function call came from. For try-except, you create you create a specific scope, where raises from the try block can only end up in the associated catch block.

This limits the complexity, and makes it therefore much easier to understand the code. If you want to test it yourself, just try to create a while loop with just try-except and raise. you can create one with comefrom:
Code: Text  [Select][+][-]
  1. COMEFROM LoopEnd
  2. // loop body
  3. if not LoopCondition then
  4.   label LoopEnd
But you can't with exceptions, because you have a well defined try-scope, in which a raise can happen, and a well defined except scope, where the raise will end. Once you jumped out of the try scope into the except scope, the try scope is inaccessible to you.


When we talk about how bad comefrom and goto are (and they are basically the same, as you can in the example above just replace COMEFROM with label and the label with goto and you have the exact same behavior), it's because its hard to keep a mental model with all the different jumping locations. Thats what the term spaghetti code means, you have a lot of interwoven strings that makes it hard to find out where one starts and where another one ends.
This is not the case with exceptions, because exceptions are extremely limited in their capabilities. You can only jump from one try block to an except block. Meaning if you want to know from where the exception can come, all you need to do is to look at your try block, which can be at most as big as the current function, so on average like 10-20 lines of code.

I admit that there is a bit of a tooling issue with Pascal. Other languages have tools which show you which functions throw which exceptions, meaning you don't even need to look into all the calls to find out where the jump can come from, you just go through every line of your try-block and look if it calls a function that can throw a certain exception. Also important for an exception is you don't need to know how deep it was triggered. And in well designed code, you can assume functions to be black boxes that perform a certain task and give a certain result, and may throw certain exceptions. You don't need to care how these functions work internally, nor what other functions they may call. From a callers point of few, an exception coming from the called function directly is not to distinquish from an exception raised by a function 3 calls down from the function you called. Therefore from all you need to know in your try-except block is which of the calls within that try block throws an exception. It is completely irrelevant which internal construct or call of that function threw the exception.
As I already said, unlike COMEFROM (or GOTO), you have a well defined scope, and you do not care about anything outside that scope.

So the idea that you don't know where a jump comes from is quite ludicrious to me. Like I said, you just need to care about 10-20 lines of code. If you can't do that, I don't think any programming technique or language feature can actually help you

alpine

  • Hero Member
  • *****
  • Posts: 738
Re: PSA: Don't use FreeAndNil
« Reply #99 on: May 26, 2023, 03:13:33 pm »
*snip*
So the idea that you don't know where a jump comes from is quite ludicrious to me. Like I said, you just need to care about 10-20 lines of code. If you can't do that, I don't think any programming technique or language feature can actually help you
You're right, no technique, and perhaps, no one, can help me explaining to you that exceptions mechanism breaks the Principle of the Least Astonishment, especially when used for making an alternative control flow.
I've tried with the humorous example of COMEFROM but apparently without success.
"I'm sorry Dave, I'm afraid I can't do that."
—HAL 9000

Warfley

  • Hero Member
  • *****
  • Posts: 1204
Re: PSA: Don't use FreeAndNil
« Reply #100 on: May 26, 2023, 03:38:18 pm »
I've tried with the humorous example of COMEFROM but apparently without success.

I know it's hard to understand, but I will try to explain it anyway: Exceptions are not COMEFROM. I don't know why you insist so much that this is the case, it simply isn't, and reasons why COMEFROM are bad don't interest a bit when you want to argue that Exceptions are bad.
Apples and Tomatoes are both fruits. If apples are sour does this mean that tomatoes are sour?

And I love that you bring up the principle of least astonishment, which is a UX principle for the end user and the end product. I'm not talking about this at all, I am talking about the lowest levels of programming, inside a function, when you perform an operation, how should you handle an exception. Note that I am not debating the existance of an exception (as an event). When a TCP stream is closed, you must be able to handle it. You can't just say "TCP stream closes don't exist" and be happy, these exceptions do exist, TCP streams do actually close in reality, and if your program can't handle these events happening, your program is errorneous.
It's the thing that I'm trying to explain for the past I don't know how many posts. Just because you don't like Exceptions, doesn't mean the events that cause those exceptions don't exist for you. And when they happen and you don't handle them, they will produce errors. So the question is just, how to notify the caller that such an event has happend and to force the caller to react appropriately (which may entail passing that information down to it's caller if necessary).

The classical way of doing so is to have a return value, and maybe a global variable with addition information, but as I have shown in the netdb example (and as every C programmer knows from heart), it happens very easiely that you forgett to check it, which results in an error, which may stay hidden in the codebase for decades. So that approach is clearly bad and the question is how to improve that.
There have been basically 2 solutions to it, one are Exceptions, coming from the OOP development, and the other is Ammending the return types, coming from the function development (and often based on algebraic type systems).
The latter is in my opinion the better solution, but it requires the typesystem of the language to support it, as well as the language having a focus on pure functions (i.e. where the main focus of the function is it's return value, not any side effects). So for modern day Pascal (or most OOP languages for that matter), this isn't really an option.

So we are only left with the latter. And it's not a perfect solution by any means, and as I have written multiple times, there has been a lot of effort in tooling and language design (e.g. the Java function signatures). But it's better than the alternative, and this is all that matters. A small improvement is still better than no improvement at all.

If you look at language design over the past few decades, the C style exception handling was phased out around 30-40 years ago. There have been new approaches that get rid of the current try-except style, often coming from the functional world (e.g. in GO, Rust and newer JavaScript), but as I said before, they are not feasable to retroactively use in Pascal. But the important thing is, I am not aware of any language that ditched the current try-catch based exceptions for old C style exceptions. So anyone who argues that you shouldn't use Exceptions (and therefore, knowingly or unknowingly that instead C style error handling should be used), massively fails to make a point, when just talking about how exceptions are bad on a very vague and high leven (as you do with your principle of least astonishment), without arguing how otherwise to address the problems exceptions solve.
Exceptions aren't perfect but history has shown they are better than the old C style alternative
« Last Edit: May 26, 2023, 03:58:39 pm by Warfley »

 

TinyPortal © 2005-2018