mboost-dp1

Hvordan FB arbejder med Tony Hoare's billion dollar "mistake"


Gå til bund
Gravatar #2 - Claus Jørgensen
23. nov. 2022 17:49
Det er faktisk underligt hvor lang tid der gik for sprog fik compile-time checked null safety

Det er blevet mainstream nu, men hvorfor det har taget over 50 år er mig en gåde.
Gravatar #3 - larsp
24. nov. 2022 08:25
Det undrer mig lidt at i deres Java eksempel:

Path getParentName(Path path) {
return path.getParent().getFileName();
}
at f.eks. getParent() returnerer Null i stedet for at kaste en exception ved problemer? Er det normal praksis i Java?
Gravatar #4 - arne_v
24. nov. 2022 14:53
#3

Exceptions kastes når der er en fejl situation.

Det er ikke en fejl situation at der ikke er en parent.

Hvis du har et klassisk binært træ i memory med backpointer så er det heller ikke en fejl at backpointer er null - det vil den altid være for top/rod noden.

Gravatar #5 - arne_v
24. nov. 2022 21:40
#4

Java og C# er iøvrigt ret enige ...


import java.io.File;
import java.io.IOException;

public class NullFun {
public static void main(String[] args) throws IOException {
File f = new File("C:\\Work\\NullFun");
do {
System.out.println(f.getCanonicalPath());
f = f.getParentFile();
} while(f != null);
}
}



using System;
using System.IO;

public class NullFun
{
public static void Main(string[] args)
{
DirectoryInfo di = new DirectoryInfo(@"C:\Work\NullFun");
do
{
Console.WriteLine(di.FullName);
di = di.Parent;
} while (di != null);
}
}

Gravatar #6 - larsp
25. nov. 2022 08:25
Python har det med at returnere samme dir igen og igen når man når til root (med pathlib). Det er ikke super kønt:

>>> from pathlib import Path
>>> Path(os.getcwd())
PosixPath('/home/lars')
>>> Path(os.getcwd()).parent
PosixPath('/home')
>>> Path(os.getcwd()).parent.parent
PosixPath('/')
>>> Path(os.getcwd()).parent.parent.parent
PosixPath('/')

#4 #5 Ja, traditionel tankegang er at exceptions kun er til deciderede fejl. Men det falder jo på et spektrum hvad man kan se som en fejl og hvad der bare er lettere usædvanligt flow i noget kode.

I Python er exceptions ikke dyre og de bliver oftere brugt til normale, lettere usædvanlige hændelser, f.eks. queue empty, eller receive timeout fra sockets.

Med køer er der faktisk gode grunde til at have en empty exception. Det gør at man kan have "None" i en kø uden problemer (Pythons svar på Null) og det kan fjerne en race condition i visse situationer. Sammenlign disse to:

if len(queue) > 0:
_ print(queue.get())
else:
_ pass # handle queue empty

vs.

try:
_ print(queue.get())
except Queue.Empty:
_ pass # handle queue empty

Der er tydeligvis en race condition i første udgave, hvis flere tråde læser fra samme queue
Gravatar #7 - larsp
25. nov. 2022 11:22
Tilsvarende, be' om lov eller be' om tilgivelse når man åbner filer:

if os.path.isfile(filename):
_ with open(filename) as f:
_ pass # Work with f
else:
_ pass # missing file

vs.

try:
_ with open(filename) as f:
_ pass # Work with f
except Exception as e:
_ pass # missing file, bad permissions or other issues

Model et har igen en race condition. Filen kunne blive slettet lige efter os.path.isfile. Model to er uden det problem og håndterer også andre problemer der måtte være ved at åbne filen.

Men er det exception værdigt at filen mangler? Det kunne sagtens være et normalt flow der ikke betragtes som en *fejl*. I Python tankegang vil man hælde til at bede om tilgivelse i stedet for at bede om lov, og det forekommer mere korrekt og robust.
Gravatar #8 - arne_v
25. nov. 2022 13:08
larsp (6) skrev:
Python har det med at returnere samme dir igen og igen når man når til root (med pathlib). Det er ikke super kønt:

>>> from pathlib import Path
>>> Path(os.getcwd())
PosixPath('/home/lars')
>>> Path(os.getcwd()).parent
PosixPath('/home')
>>> Path(os.getcwd()).parent.parent
PosixPath('/')
>>> Path(os.getcwd()).parent.parent.parent
PosixPath('/')



Jeg kastede faktisk et blik på Python inden jeg postede.

Og tænkte WTF.

Det kunne nemt ende med en uendelig løkke.

larsp (6) skrev:

#4 #5 Ja, traditionel tankegang er at exceptions kun er til deciderede fejl. Men det falder jo på et spektrum hvad man kan se som en fejl og hvad der bare er lettere usædvanligt flow i noget kode.

I Python er exceptions ikke dyre og de bliver oftere brugt til normale, lettere usædvanlige hændelser, f.eks. queue empty, eller receive timeout fra sockets.


Exceptions giver tit kortere kode. Og det værsætter man traditionelt i Python verdenen.

Og p.g.a. den typiske fortolkning (CPython) er det relative overhead af try catch minimalt.

larsp (6) skrev:

Med køer er der faktisk gode grunde til at have en empty exception. Det gør at man kan have "None" i en kø uden problemer (Pythons svar på Null) og det kan fjerne en race condition i visse situationer. Sammenlign disse to:

if len(queue) > 0:
_ print(queue.get())
else:
_ pass # handle queue empty

vs.

try:
_ print(queue.get())
except Queue.Empty:
_ pass # handle queue empty

Der er tydeligvis en race condition i første udgave, hvis flere tråde læser fra samme queue


Den første kode er et standard eksempel på usikker kode.

Men bemærk at i Java og C# har man frit valg.

Java: for implementationer af java.util.Queue kan man vælge mellem .poll() som returnerer null hvis tom og .remove() som smider exception hvis tom. Hvis det er en implementation af java.util.concurrent.BlockingQueue er der en .take() der venter indtil køen ikke er tom.

C#: System.Collections.Generic.Queue har en .Dequeue() som smider exception hvis tom og .TryDequeue(out o) som returnerer false hvis top (og returnerer true og objekt i out argument hvis ikke tom).

Gravatar #9 - arne_v
25. nov. 2022 13:14
#7

Java og C# smider også exception hvis en input fil ikke eksisterer.

Om det er fordi man betragter det som en fejl eller om det er p.g.a. det praktiske problem at en constructor ikke kan returnerer null ved jeg ikke.
Gravatar #10 - Claus Jørgensen
25. nov. 2022 16:47
#9

Constructors i Swift kan returnere null :) (nil)
Gravatar #11 - arne_v
25. nov. 2022 18:09
#10

Hvad.

Skal man så teste for det efter construction?
Gravatar #12 - Claus Jørgensen
26. nov. 2022 13:54
Gravatar #13 - arne_v
26. nov. 2022 15:01
#12

Hmmm.

Jeg er ikke varm på konceptet.

Jeg synes at det giver endnu noget som udvikleren skal tænke på.

Hvis man ikke bryder sig om exception men foretrækker en retur værdi så kunne man flytte den kritiske logik fra constructor til en actualinit metode som returnerer true/false.
Gravatar #14 - Claus Jørgensen
26. nov. 2022 16:15
#13

Det fungere rigtigt fint i Swift fordi at `if let` konstruktionen bruges hele tiden. Plus, det er compile-time checked, så der er ikke rigtig noget man tænker over.

Og det bruges mest til value convertion, i.e. string til enum, eller string til integer, etc.

`if let intValue = Int(stringValue) { do something } else { not-a-valid-int }` er rigtig nemt at arbejde med.
Gravatar #15 - arne_v
26. nov. 2022 18:24
#14

int intValue;
if(int.TryParse(stringvalue, out intValue))
{
// do something
}
else
{
// not a valid int
}

I Java hænger man på catch NumberFormatException. Hvilket de fleste synes er forkert.

Gravatar #16 - arne_v
26. nov. 2022 21:41
arne_v (15) skrev:

I Java hænger man på catch NumberFormatException. Hvilket de fleste synes er forkert.


Følgende kode lavede jeg i 2003 (!) og der ere faktisk ikke sket noget siden.


public class IntegerTest {
private static final int antal = 100000000;

private static final String goodtext = "12345";
private static final String badtext = "12345a";

private static void test(String text) {
long start1 = System.currentTimeMillis();
for (int i = 0; i < antal; i++) {
int tal = test1(text);
}
long stop1 = System.currentTimeMillis();
long start2 = System.currentTimeMillis();
for (int i = 0; i < antal; i++) {
int tal = test2(text);
}
long stop2 = System.currentTimeMillis();
long start3 = System.currentTimeMillis();
for (int i = 0; i < antal; i++) {
int tal = test3(text);
}
long stop3 = System.currentTimeMillis();
System.out.println("test 1 (try/catch) = " + (stop1 - start1));
System.out.println("test 2 (homemade)= " + (stop2 - start2));
System.out.println("test 3 (isDigit()) = " + (stop3 - start3));
}
private static int test1(String s) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
return -1;
}
}
private static int test2(String s) {
for (int j = 0; j < s.length(); j++) {
if (s.charAt(j) < '0' || s.charAt(j) > '9') {
return -1;
}
}
return Integer.parseInt(s);
}
private static int test3(String s) {
for (int j = 0; j < s.length(); j++) {
if (!Character.isDigit(s.charAt(j))) {
return -1;
}
}
return Integer.parseInt(s);
}
public static void main(String[] args) {
System.out.println("All good:");
test(goodtext);
System.out.println("All bad:");
test(badtext);
}
}


C:\Work>javac IntegerTest.java

C:\Work>java IntegerTest
All good:
test 1 (try/catch) = 1840
test 2 (homemade)= 3180
test 3 (isDigit()) = 3510
All bad:
test 1 (try/catch) = 121050
test 2 (homemade)= 2080
test 3 (isDigit()) = 1600
Gravatar #17 - larsp
27. nov. 2022 08:10
@Failable initializers

Det kunne man da nemt med gør-det-selv objektorienteret programmering i C ;)

@Exceptions generelt

Hvad mener d'herrer om Go's beslutning om helt at droppe exceptions? https://go.dev/doc/faq#exceptions

Det svarer jo til at gå tilbage til C, hvor man tilsvarende må klare det hele selv med test af returværdier for nærmest alle funktionskald. I C er det en naturlig ting, for der er aldrig noget secret sauce gemt bag kodelinjerne. Exception handling vil bryde med idéen om "what you code is what you get", som vel er en af de få tilbageværende forcer ved C.

I moderne sprog forstår jeg ikke den kalkyle der fører til at droppe exceptions. Min erfaring i Python er at det gør koden mere elegant og sparer mange linjer.
Gravatar #18 - larsp
27. nov. 2022 08:53
#16 Det var godt nok en stor performance penalty ved exception handling. Og gør-det-selv metoderne er ikke komplette, for hvad med startende minus for negative tal? Og hvad med floating point, så må man vist hellere give op med den slags.

(men jeg skal ikke komme for godt igang, jeg har ind imellem kodet custom versioner af atoi i C, i frustration over at der ikke er support for error handling og udgaver der returnerer en specifik int / uint størrelse)
Gravatar #19 - arne_v
27. nov. 2022 13:41
larsp (17) skrev:

@Exceptions generelt

Hvad mener d'herrer om Go's beslutning om helt at droppe exceptions? https://go.dev/doc/faq#exceptions

Det svarer jo til at gå tilbage til C, hvor man tilsvarende må klare det hele selv med test af returværdier for nærmest alle funktionskald. I C er det en naturlig ting, for der er aldrig noget secret sauce gemt bag kodelinjerne. Exception handling vil bryde med idéen om "what you code is what you get", som vel er en af de få tilbageværende forcer ved C.

I moderne sprog forstår jeg ikke den kalkyle der fører til at droppe exceptions. Min erfaring i Python er at det gør koden mere elegant og sparer mange linjer.


Jeg mener heller ikke at Rust har exceptions. Og jeg gætter på at Hare og Zig heller ikke har det.

Exceptions giver mere mening i high level business logic kode end i low level HW nær kode.

Man skal lige huske på hvad den store forskel på en retur værdi og en exception er. Hvis man catcher exception lige efter kaldet så er forskellen meget lille. Den store forskel er at exceptions kan catches 1-2-3-5-10-25 kald tilbage i kaldestakken uden at al den mellemliggende kode skal gøre noget.

Så hvis man er tilfreds med en logik:
- der skete en exception
- vi returnerer til et godt godt recovery punkt
- runtime og OS sørger for memory, filer, devices etc. er OK
- vi kører bare videre fra recovery punkt
så er exceptions rigtigt gode.

Men er man i et miljø hvor koden selv skal håndtere en masse ting, så vil exceptions enten kræve en masse setup i genereret kode og runtime eller risikere at efterlade tingene fubar.

Og C har faktisk en primitiv exception mekanisme: setjmp og longjump. Men de har aldrig været populære. Af gode grunde efter min mening.

Gravatar #20 - arne_v
27. nov. 2022 13:48
larsp (18) skrev:

Det var godt nok en stor performance penalty ved exception handling.


Når koden er blevet JIT compilet og kører derudaf, så er det relative exception overhead stort.

I Python (tradition CPython) vil jeg forvente at det relative exception overhead er langt mindre.

larsp (18) skrev:

Og gør-det-selv metoderne er ikke komplette, for hvad med startende minus for negative tal? Og hvad med floating point, så må man vist hellere give op med den slags.


"123.456" er tilsigtet at fejle og det feljler også med Integer.parseInt.

"-123" virker naturligvis med Integer.parseInt, så der er et gab i funktionalitet. Et fix vil kræve at logikken med at returnerer -1 ved fejl ændres. Så ...
Gravatar #21 - arne_v
27. nov. 2022 18:19
#19

longjmp ikke longjump.
Gravatar #22 - arne_v
27. nov. 2022 18:23
#19

Interessant emne iøvrigt.

Lidt eksempler.

Først de "gode" exception eksempler.

Java:


package november;

public class ErrorHandling {
public static class ManagedData {
public ManagedData() {
System.out.println("ManagedData memory allocated - GC will deallocate");
}
}
public static class NativeResources implements AutoCloseable {
public NativeResources() {
System.out.println("NativeResources allocated");
}
@Override
public void close() {
System.out.println("NativeResources deallocated");
}
}
public static class MyException extends RuntimeException {
}
private static void m3() {
ManagedData o = new ManagedData();
System.out.println("**** Error ****");
throw new MyException();
}
private static void m2() {
try(NativeResources o = new NativeResources()) {
m3();
System.out.println("Normal return");
}
}
private static void m1() {
ManagedData o = new ManagedData();
m2();
System.out.println("Normal return");
}
public static void main(String[] args) {
try {
m1();
System.out.println("Normal return");
} catch(MyException ex) {
System.out.println("GC will deallocate all memory");
}
}
}


C#:


using System;

namespace November
{
public class Program
{
public class ManagedData
{
public ManagedData()
{
Console.WriteLine("ManagedData memory allocated - GC will deallocate");
}
}
public class NativeResource : IDisposable
{
public NativeResource()
{
Console.WriteLine("NativeResource allocated");
}
public void Dispose()
{
Console.WriteLine("NativeResource deallocated");
}
}
public class MyException : Exception
{
}
public static void M3()
{
ManagedData o = new ManagedData();
Console.WriteLine("**** Error ****");
throw new MyException();
}
public static void M2()
{
using(NativeResource o = new NativeResource())
{
M3();
Console.WriteLine("Normal return");
}
}
public static void M1()
{
ManagedData o = new ManagedData();
M2();
Console.WriteLine("Normal return");
}
public static void Main(string[] args)
{
try
{
M1();
Console.WriteLine("Normal return");
}
catch(MyException)
{
Console.WriteLine("GC will deallocate all memory");
}
Console.ReadKey();
}
}
}


Python:


class ManagedData(object):
def __init__(self):
print('ManagedData memory allocated - GC will deallocate')

class NativeResource(object):
def __init__(self):
print('NativeResource allocated')
def __enter__(self):
return self
def __exit__(self, *args):
print('NativeResource deallocated')

class MyException(Exception):
pass

def m3():
o = ManagedData()
print('**** Error ****')
raise MyException()

def m2():
with NativeResource() as o:
m3()
print('Normal return')

def m1():
o = ManagedData()
m2()
print('Normal return')

try:
m1()
print('Normal return')
except MyException:
print('GC will deallocate all memory')


Så den gammeldags måde med retur værdi.

C:


#include <stdio.h>
#include <stdlib.h>

#define TRUE 1
#define FALSE 0

struct Data
{
};

static void os_ressource_allocate()
{
printf("OS resource allocated\n");
}

static void os_ressource_deallocate()
{
printf("OS resource deallocated\n");
}

static int m3()
{
struct Data *o;
printf("Data memory allocated\n");
o = malloc(sizeof(struct Data));
printf("**** Error ****\n");
printf("Data memory deallocated\n");
free(o);
return FALSE;
}

static int m2()
{
os_ressource_allocate();
if(!m3())
{
os_ressource_deallocate();
return FALSE;
}
os_ressource_deallocate();
printf("Normal return\n");
return TRUE;
}

static int m1()
{
struct Data *o;
printf("Data memory allocated\n");
o = malloc(sizeof(struct Data));
if(!m2())
{
printf("Data memory deallocated\n");
free(o);
return FALSE;
}
printf("Data memory deallocated\n");
free(o);
printf("Normal return\n");
return TRUE;
}

int main(int argc, char *argv[])
{
if(!m1())
{
printf("Hopefully everything has been deallocated\n");
return 0;
}
printf("Normal return\n");
return 0;
}


Og så setjmp-longjmp der ikke duer.


#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

static jmp_buf ctx;

struct Data
{
};

static void os_ressource_allocate()
{
printf("OS resource allocated\n");
}

static void os_ressource_deallocate()
{
printf("OS resource deallocated\n");
}

static void m3()
{
struct Data *o;
printf("Data memory allocated\n");
o = malloc(sizeof(struct Data));
printf("**** Error ****\n");
printf("Data memory deallocated\n");
free(o);
longjmp(ctx, 1);
return;
}

static void m2()
{
os_ressource_allocate();
m3();
os_ressource_deallocate();
printf("Normal return\n");
return;
}

static void m1()
{
struct Data *o;
printf("Data memory allocated\n");
o = malloc(sizeof(struct Data));
m2();
printf("Data memory deallocated\n");
free(o);
printf("Normal return\n");
return;
}

int main(int argc, char *argv[])
{
if(setjmp(ctx))
{
printf("Hopefully everything has been deallocated\n");
return 0;
}
else
{
m1();
printf("Normal return\n");
return 0;
}
}

Gravatar #23 - larsp
29. nov. 2022 17:00
#21 setjmp og longjmp i C, uhhhh, det er heldigvis noget jeg ikke er stødt på i min karriere. Jeg har kun set det i eksempel kode online. Det er vist et relikvie der bedst bør forblive låst inde i museumsskabet.

malloc i C er lidt svær. Man bør i princippet altid checke for NULL returværdi, men sagen er jo at hvis mallocs begynder at fejle her og der, så er ens system totalt hosed, og ikke til at redde alligevel. Det er næsten bedre at det bare segfaulter med en NULL dereference, end at det hoster videre på 3 cylindere med alle mulige ikke gennemtænkte resultater. Det er muligt, men svært at skrive malloc fail-safe kode for en stort projekt.

I embedded er det normalt at have logging på og overvåge heap og stack forbrug over et stykke tid og i alle tænkelige use-cases. Begge skal være på den klart sikre sidde af at løbe fuld.
Gravatar #24 - larsp
29. nov. 2022 17:33
... og i mere kritiske systemer undgår man helt heap. Eller man kan vælge at vedtage nogle begrænsninger, f.eks. at alle mallocs skal ske under opstart og så er det ellers forbudt.
Gravatar #25 - arne_v
29. nov. 2022 18:24
#23

setjmp og longjmp er meget sjældne i den virkelige verden.

Jeg har faktisk noget midt 90'er C++ kode som bruger det. Det var praktisk til at g[ tilbage i kaldestakken og det var før C++ fik exceptions (det skete f'rst med C++98).

Gravatar #26 - arne_v
29. nov. 2022 18:26
#24

Jeg har arbejdet på en multi-MLOC C kode base for server uden dynamisk memory allokering netop fordi at det var et forretningskritisk system (og fordi en del af de oprindelige udviklere kom fra Fortran!).

Gravatar #27 - Claus Jørgensen
29. nov. 2022 22:59
arne_v (15) skrev:
#14

int intValue;
if(int.TryParse(stringvalue, out intValue))
{
// do something
}
else
{
// not a valid int
}


Fordelen ved Swift's måde at gøre det på er at du ikke skal bruge en ekstra variabel som er declareret før dit if-statement, og at du ikke har nogen risiko for en mutable variable.

Og problemet med en out variable er at den altid vil være mutable. Hvilket er noget vi helst forsøger at undgå.
Gå til top

Opret dig som bruger i dag

Det er gratis, og du binder dig ikke til noget.

Når du er oprettet som bruger, får du adgang til en lang række af sidens andre muligheder, såsom at udforme siden efter eget ønske og deltage i diskussionerne.

Opret Bruger Login