Skrivet av klk:
Såg du mitt exempel tidigare om en tabell som skrevs ut, det var ingen slump att jag la upp den koden men det verkar inte som någon nappar. Nu med LLMer är det himla smidigt och testa hur funktionalitet skrivs med andra språk, inte alls svårt.
Har inte sett att andra språk gör det enklare. Framförallt ser det "konstigt" ut. För något som C++ verkligen lyckats med är att det går att få ett mer naturligt utseende, det behövs inte lika mycket omskrivningar/anpassningar för att få till funktionaliteten. Och här sticker verkligen RUST ut, språkets tvingar utvecklaren skriva kod annorlunda.
OOP språk (C#, Java) lider också kraftigt av det problemet, att allt skall ligga i objekt är ett allvarligt designfel. Exempelvis svårt att avgöra vad som är bra skrivna objekt och vad som är hack eftersom allt MÅSTE ligga i objekt.
Ett problem med LLMs är att skit in blir tyvärr skit ut. Det verkar som du har rätt liten kunskap om t.ex. Rust. Blir då väldigt stor risk att det du får från en LLM är rätt exakt vad du frågade efter, men p.g.a. bristande kunskap om Rust ställer du fel fråga!
Vi håller oss till ett av dina favoritämnen, tagged unions. Du är medveten om att Rust har inbyggt stöd för detta i språket, till skillnad från t.ex. C++ som har det via bibliotek (kör du boost:.variant eller std::variant?).
Om vi tar ett superenkelt fall och sedan jämför det med något som gör motsvarande på ett idiomatiskt sätt i Rust och C#. Definiera en "tagged union" som i alla fall kan hålla int, double och sträng. Vi gör sedan en funktion som adderar två sådana.
C++
#include <optional>
#include <string>
#include <variant>
using MyVariant = std::variant<int, double, std::string>;
int variant_sum_may_throw(MyVariant a, MyVariant b)
{
return std::get<int>(a) + std::get<int>(b);
}
std::optional<int> variant_sum(const MyVariant& a, const MyVariant& b)
{
if (std::holds_alternative<int>(a) && std::holds_alternative<int>(b))
{
return std::get<int>(a) + std::get<int>(b);
}
return std::nullopt;
}
int sum(int a, int b)
{
return a + b;
}
Rust
pub enum MyVariant {
Int(i32),
Double(f64),
Text(String),
}
pub fn variant_sum_may_panic(a: MyVariant, b: MyVariant) -> i32 {
match (a, b) {
(MyVariant::Int(x), MyVariant::Int(y)) => x + y,
_ => panic!("Both arguments must be Int"),
}
}
pub fn variant_sum(a: &MyVariant, b: &MyVariant) -> Option<i32> {
if let (MyVariant::Int(x), MyVariant::Int(y)) = (a, b) {
Some(x + y)
} else {
None
}
}
pub fn sum(a: i32, b: i32) -> i32 {
a + b
}
C# har också inbyggt stöd för "tagged unions" i form av nyckelordet "dynamic" som är som en variant man kan stoppa in alla typer i. Den "vinner" om man räknar "hur lite extra lär man skriva för att använda detta"?
int add(dynamic a, dynamic b)
{
return a + b;
}
Skrivet av klk:
Det där är bara okunskap, och det är självklart eftersom den typen av kod är så ovanlig. De som lärt sig, där tror jag många likt mig undrar varför inte fler använder sig av tekniken med tanke på hur mycket det effektiviserar.
Tror det beror på att svårigheterna att diskutera teknik idag, går i princip inte om det blir svårare i allmänna/större grupper.
Två helt olika saker, statisk typning är att kompilatorn vet vad det är. Har inget med vad tagged unions försöker lösa. Templates som exempelvis kan upplevas som något som liknar, är bara att kompilatorn klarar generera kod. Är det fel på data i runtime har du inte en kompilator som kan kontrollera.
Tagged unions, då vet koden vad det är (maskinkoden)
Har bara använt std::variant<> någon enstaka gång. Har använt enums med associerade värden i Rust oftare, men fortfarande inget som är vanligt förkommande.
För att precisera frågan lite då. Ibland jagar du saker som (du i alla fall hoppas) skalar bort enskilda assemblerinstruktioner. Är inte det en stark orsak att försöka undvika tagged unions om man kan, för kompilatorn kan göra rätt mycket enklare kod när den vet vad typen är compile-time.
Om vi tar fallen
// C++
int sum(int a, int b)
{
return a + b;
}
// Rust
pub fn sum(a: i32, b: i32) -> i32 {
a + b
}
så blir det
Tar vi fallen
// C++
std::optional<int> variant_sum(const MyVariant& a, const MyVariant& b)
{
if (std::holds_alternative<int>(a) && std::holds_alternative<int>(b))
{
return std::get<int>(a) + std::get<int>(b);
}
return std::nullopt;
}
// Rust
pub fn variant_sum(a: &MyVariant, b: &MyVariant) -> Option<i32> {
if let (MyVariant::Int(x), MyVariant::Int(y)) = (a, b) {
Some(x + y)
} else {
None
}
}
så har det overhead för att kolla taggen värde
// C++
variant_sum(std::variant<int, double, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > const&, std::variant<int, double, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > const&):
ldrb w2, [x0, 32]
sub sp, sp, #16
cbnz w2, .L14
ldrb w2, [x1, 32]
cbz w2, .L17
.L14:
str xzr, [sp, 8]
ldr x0, [sp, 8]
add sp, sp, 16
ret
.L17:
ldr w0, [x0]
mov w2, 1
ldr w1, [x1]
strb w2, [sp, 12]
add w0, w0, w1
str w0, [sp, 8]
ldr x0, [sp, 8]
add sp, sp, 16
ret
// Rust
variant_sum:
ldr x9, [x0]
mov x8, #-9223372036854775808
ldr x10, [x1]
cmp x9, x8
ldr w9, [x1, #8]
ccmp x10, x8, #0, eq
ldr w8, [x0, #8]
cset w0, eq
add w1, w9, w8
ret
Dold text
Helt OK overhead att ta om den behövs, men är det vettigt att köra som nära nog standardval som du verkar göra?
Sen har du och Casey Muratori en sak gemensamt, ni båda beskriver fördelar med DOD från en svunnen tid. Det FINNS fördelar än idag, men de Casey specifikt pratar om i videon du länkar var högst relevanta på de tidiga konsolerna som typiskt hade rätt kraftig HW i förhållande till mängden RAM.
Saker som tagged union var då högst värdefulla för att ha en bra metod att utnyttja det RAM (ibland kunde det vara riktigt lite minne som i PS2 om man ville utnyttja scratchpad på 16 kB) på ett optimalt sätt. Just det är inte längre en flaskhals ens för konsoler och definitivt inte för typiska server-plattformar.
Däremot finns det andra saker med DOD som är värdefullt idag, t.ex. att det kan göra det långt enklare att hantera en minneslayout som lämpar sig väl att processa med SIMD. Fast det använder du ju inte, autovektorisering är tyvärr långt ifrån tillräckligt för att få utväxling på SIMD. I bästa fall finns bra bibliotek, men idag får man tyvärr nog acceptera att skriva intrinsics eller köra t.ex. ISPC (vilket bl.a. Unreal Engine använder).
Så är rätt oklart vilken vinst du uppnår med DOD. Måste fortfarande helt missat något, för fattar inte alls vad storheten är i den kod du visat så här långt. Framförallt inte hur den använder DOD på något vettig sätt.