I've been looking at the use of Option type.
This means converting a function like:
Customer GetCustomerById(Int32 customerID) {...}
Customer c = GetCustomerById(619);
DoStuff(c.FirstName, c.LastName);
to return a option Maybe
type:
Maybe<Customer> GetCustomerById(Int32 customerID) {...}
in my non-functional language, i then have to check if the return value is present:
Maybe<Customer> c = GetCustomerById(619);
if (c.HasValue)
DoStuff(c.Value.FirstName, c.Value.LastName);
And that works well enough:
null
(rather than raising an exception)But I'm not in C#, Java, or C++ with its RAII. I'm in Delphi; a native language with manual memory management. I will continue to show code examples in a C#-like language.
With manual memory management, my original code:
Customer c = GetCustomerById(619);
if (c != nil)
{
try
{
DoStuff(c.FirstName, c.LastName);
}
finally
{
c.Free();
}
}
gets converted to something like:
Maybe<Customer> c = GetCustomerById(619);
if (c.HasValue)
{
try
{
DoStuff(c.Value.FirstName, c.Value.LastName);
}
finally
{
c.Value.Free();
}
}
I now have a Maybe<>
holding onto a reference that is invalid; it's worse than null because now the Maybe
thinks it has valid contents, and the contents do have a pointer to memory, but that memory isn't valid.
I've traded a possible NullReferenceException
to a random data-corruption crash bug.
Has anyone thought about this issue, and techniques to work around it?
I have thought about adding a method to the struct called Free
:
void Free()
{
if (this.HasValue())
{
_hasValue = false;
T oldValue = _value;
_value = null;
oldValue.Free();
}
}
Which does work if people call it; and know to call it; know why to call it; and know what they shouldn't be calling.
A lot of subtle knowledge to avoid a dangerous bug that i only introduced by trying to use an option type.
It also falls apart when the object being wrapped in the Maybe<T>
is actually destroyed indirectly through a method not named canonical Free
:
Maybe<ListItem> item = GetTheListItem();
if item.HasValue then
begin
DoStuffWithItem(item.Value);
item.Value.Delete;
//item still thinks it's valid, but is not
item.Value.Selected := False;
end;
The Nullable
/Maybe
/Option
type has virtue for when dealing with types that have no in-built non-value (e.g. records, integers, strings, where there is no in-built non-value).
If a function returns a non-nullable value, then there's no way to communicate the non-presence of a return result without using some special sentinal values.
function GetBirthDate(): TDateTime; //returns 0 if there is no birth date
function GetAge(): Cardinal; //returns 4294967295 if there is no age
function GetSpouseName: string; //returns empty string if there is no spouse name
The Option is used to avoid special sentinel values, and communicate to the caller what is really going on.
function GetBirthDate(): Maybe<TDateTime>;
function GetAge(): Maybe<Integer>;
function GetSpouseName: Maybe<string>;
The Option
type has also gained popularity a way to avoid NullReferenceExceptions
(or EAccessViolation
at address $00000000) by separating thing with no thing.
Functions returning special, sometimes dangerous, sentinal values
function GetBirthDate(): TDateTime; //returns 0 if there is no birth date
function GetAge(): Cardinal; //returns 4294967295 if there is no age
function GetSpouseName: string; //returns empty string if there is no spouse name
function GetCustomer: TCustomer; //returns nil if there is no customer
Are converted to forms where the special, sometimes dangerous, sentinal values are impossible:
function GetBirthDate(): Maybe<TDateTime>;
function GetAge(): Maybe<Integer>;
function GetSpouseName: Maybe<string>;
function GetCustomer: Maybe<TCustomer>;
Callers are made to realize the function can return no thing, and they must o go though the hoop of checking for existence. In the case of types that already support being null, the Option
gives us a chance to try to stop people from causing NullReference exceptions.
In functional programming languages it is much more robust; the return type can be constructed so it is impossible to return a nil
- the compiler just won't allow it.
In procedural programming languages the best we can do is lock away the nil
, and make it impossible to reach. And in the process the caller has more robust code.
One could argue "why not tell the developer to never make mistakes":
Bad:
customer = GetCustomer();
Print(customer.FirstName);
Good:
customer = GetCustomer();
if Assigned(customer)
Print(customer.FirstName);
Just get good.
The problem is i want the compiler to catch these mistakes. I want mistakes to be harder to happen in the first place. I want a pit of success. It forces the caller to understand that the function might fail. The signature itself explains what to do, and makes dealing with it easy.
In this case we are implicitly returning two values:
People in functional programming languages have adopted the concept, and it is a concept that people are trying to bring back into procedural languages, that you have a new type that conveys if the value is there or not. And attempts to blindly use it will give a compile-time error:
customer = GetCustomer();
Print(customer.FirstName); //syntax error: Unknown property or method "FirstName"
If you want to learn more about attempts to use the functional Maybe
monad in procedural languages, you can consult more thoughts on the subject:
The only thing that can protect you from calling anything on your Value
wrapped in Maybe
is to extract value out of the Maybe
variable clearing the Maybe
content and using the result like you would normally use any object reference.
Something like:
TMaybe<T> = record
strict private
FValue: T;
public
...
function ExtractValue: T;
end;
function TMaybe<T>.ExtractValue: T;
begin
if not _hasValue then raise Exception.Create('Invalid operation, Maybe type has no value');
Result := FValue;
_hasValue = false;
FValue := Default(T);
end;
And then you would be forced to Extract value in order to use it.
Maybe<ListItem> item = GetTheListItem();
if item.HasValue then
begin
Value := item.ExtractValue;
DoStuffWithItem(Value);
Value.Free;
end;
You don't need Maybe
in Delphi, as objects are reference types, so you can use nil
object pointers, eg:
type
TCustomer = class
public
customerID: Int32;
FirstName, LastName: string;
end;
function GetCustomerById(customerID: Int32): TCustomer;
begin
...
if (customerID is found) then
Result := ...
else
Result := nil;
end;
var
c: TCustomer;
begin
c := GetCustomerById(619);
if c <> nil then
DoStuff(c.FirstName, c.LastName);
end;
If the function needs to allocate a new object for return, eg:
function GetCustomerById(customerID: Int32): TCustomer;
begin
...
if (customerID is found) then
begin
Result := TCustomer.Create;
...
end else
Result := nil;
...
end;
Then you have two choices for lifetime management (assuming the caller needs to take ownership of the object because it is not owned elsewhere).
1) you can call Free
when you are done using the object:
var
c: TCustomer;
begin
c := GetCustomerById(619);
if c <> nil then
try
DoStuff(c.FirstName, c.LastName);
finally
c.Free;
end;
end;
2) you can use a reference-counted interface:
type
ICustomer = interface
['{2FBD7349-340C-4A4E-AA72-F4AD964A35D2}']
function getCustomerID: Int32;
function getFirstName: string;
function getLastName: string;
property CustomerID: Int32 read getCustomerID;
property FirstName: string read getFirstName;
property LastName: string read getLastName;
end;
TCustomer = class(TInterfacedObject, ICustomer)
public
fCustomerID: Int32;
fFirstName, fLastName: string;
function getCustomerID: Int32;
function getFirstName: string;
function getLastName: string;
end;
function TCustomer.getCustomerID: Int32;
begin
Result := fCustomerID;
end;
function TCustomer.getFirstName: string;
begin
Result := fFirstName;
end;
function TCustomer.getLastName: string;
begin
Result := fLastName;
end;
function GetCustomerById(customerID: Int32): ICustomer;
begin
...
if (customerID is found) then
begin
Result := TCustomer.Create as ICustomer;
...
end else
Result := nil;
end;
var
c: ICustomer;
begin
c := GetCustomerById(619);
if c <> nil then
DoStuff(c.FirstName, c.LastName);
end;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With