mboost-dp1
Hvordan FB arbejder med Tony Hoare's billion dollar "mistake"
- Forside
- ⟨
- Forum
- ⟨
- Tagwall
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.
Det er blevet mainstream nu, men hvorfor det har taget over 50 år er mig en gåde.
#4
Java og C# er iøvrigt ret enige ...
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);
}
}
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:
#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:
vs.
Der er tydeligvis en race condition i første udgave, hvis flere tråde læser fra samme queue
>>> 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
Tilsvarende, be' om lov eller be' om tilgivelse når man åbner filer:
vs.
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.
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.
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).
#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.
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.
#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.
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.
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
@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.
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.
#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)
(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)
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.
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å ...
#19
Interessant emne iøvrigt.
Lidt eksempler.
Først de "gode" exception eksempler.
Java:
C#:
Python:
Så den gammeldags måde med retur værdi.
C:
Og så setjmp-longjmp der ikke duer.
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;
}
}
#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.
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.
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å.
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.