Proposal for C language …
N3694
Functions with Data - Closures in C (A Comprehensive Proposal Overviewing Blocks, Nested Functions, and Lambdas)
Free on Packt today … Free Learning | Daily Programming eBook from Packt
Learn Type-Driven Development
I believe I have missed this contribution, up until now …
@colinj Colin Johnsun
https://forums.adug.org.au/t/linq-for-delphi/54400
function IsEven(I: Integer): Boolean;
begin
Result := I mod 2 = 0;
end
function Add12(I: Integer): Integer;
begin
Result := I + 12;
end;
procedure Main;
var
Numbers: TArray<Integer>;
begin
Numbers := TArray<Integer>.Create(1,2,3,3,4,5,8,10,11,12,13,14,14,18,20)
TSeq.From<Integer>(Numbers)
.Where(IsEven) // [2, 4, 8, 10, 12, 14, 14, 18, 20]
.Select(Add12) // [14, 16, 20, 22, 24, 26, 26, 30, 32]
.Skip(3) // [22, 24, 26, 26, 30, 32]
.Take(4) // [22, 24, 26, 26]
.ForEach(WriteLn);
end;
(That was a quote of a comment from Opus 5)
Thought for 35s
The core problem is that Maybe earns its keep through unrepresentable states, and a record with a boolean flag represents the bad state perfectly well — it just asks you nicely not to look at it.
The giveaway is Value. In a real sum type there is no projection from Option<T> to T; you can only get at the payload by destructuring, and the compiler makes you say what happens in the other arm. Delphi’s version exposes Value as an ordinary property, so the best it can do is raise on access when HasValue is false. That converts a null dereference into an exception, which is an improvement in diagnosis but not in kind — it’s still a runtime failure at the point of use, precisely what the type was supposed to make impossible. The type system is uninvolved.
Most implementations then compound it:
class operator Implicit(const A: Nullable<T>): T;
which makes the unwrapping silent and invisible. Pass a Nullable<Integer> to something taking an Integer and it compiles, with the check happening somewhere you can’t see. That single operator gives back the entire hole the abstraction was built to close. It’s there for ergonomics, and I understand why, but a Maybe<T> that is substitutable for T isn’t a Maybe — it’s a nullable pointer with extra steps.
There’s also an initialisation trap people hit less often than they should. Local records aren’t zero-filled; _InitializeRecord only walks managed fields. So for Nullable<string>, FValue gets cleared because it’s a string, while FHasValue — a plain Boolean — inherits whatever was on the stack. You get a Nullable that claims to hold a value and doesn’t.
Custom Managed Records in 10.4 fixed this properly:
class operator Initialize(out Dest: Nullable<T>);
begin
Dest.FHasValue := False;
end;
That forces the record to be managed and guarantees the flag. Worth checking whether whatever implementation you’re using predates it, because a lot of the hand-rolled ones floating around do.
Then there’s composition, which is where the difference gets practical rather than theoretical. Maybe is useful because map and bind chain without ever unwrapping. You can write those here, but with no inference for generic method arguments you must annotate every step — x.Map<string>(...).Bind<Integer>(...) — and with no user-definable operators for bind, anything non-trivial nests into unreadability. So people don’t chain; they check HasValue and unwrap, and you’re back where you started.
And the reason nobody just builds a proper sum type is that Delphi’s only native tagged-union construct is the variant record, which explicitly forbids managed types in variant parts — no strings, interfaces, or dynamic arrays — exactly because the compiler can’t know which arm is live and therefore can’t emit correct finalisation. So the one language feature shaped like a discriminated union is unusable for every case you’d actually want one for. That’s why Nullable, Either, and Result all end up as separate hand-rolled records with the same weaknesses rather than instances of one pattern. And with nothing in the RTL, Spring4D’s, FireDAC’s, TMS’s, and your own don’t interoperate.
One genuinely nice consequence of the thing we were just disassembling, though: safe unwrapping is now expressible inline for the first time.
Total := Total + (if Qty.HasValue then Qty.Value else 0);
That was impossible with IfThen, because eager argument evaluation would touch Qty.Value on the empty case and raise. Laziness is what makes the guard actually guard. It’s still structurally a check-then-project rather than a destructure, so it doesn’t fix the soundness story — but it makes the correct idiom short enough that people might use it, which counts for something.