Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What about when the Object inside the option type is destroyed?

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:

  • you know from the function signature whether it can return a null (rather than raising an exception)
  • you are poked into checked the returned value before blindly using it

But there's no garbage collection

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?

Add .Free to the Maybe

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;

Bonus Chatter

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>;

Not just for non-nullable types

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:

  • a customer
  • a flag indicating if the customer is really there

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"

Bonus Reading

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:

  • Oracle Java: Tired of Null Pointer Exceptions? Consider Using Java SE 8's Optional!
  • Delphi Sorcery: Never return nil? Maybe!
  • Removing Null from C#
like image 298
Ian Boyd Avatar asked Aug 24 '17 18:08

Ian Boyd


2 Answers

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;
like image 165
Dalija Prasnikar Avatar answered Oct 12 '22 22:10

Dalija Prasnikar


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;
like image 45
Remy Lebeau Avatar answered Oct 13 '22 00:10

Remy Lebeau