Three valued logic in Delphi
Asked Answered
D

2

14

How to best implement a three valued logic in Delphi?

I was thinking of

type
  TExtBoolean = (ebTrue, ebFalse, ebUnknown);

with

function ExtOr(A: TExtBoolean; B: TExtBoolean): TExtBoolean;
begin
  if (A = ebTrue) or (B = ebTrue) then
    Result := ebTrue
  else if (A = ebFalse) and (B = ebFalse) then
    Result := ebFalse
  else
    Result := ebUnknown;
end;

and so on.

But that does not seem to be very elegant. Does a better way exist?

Edit: With elegance I mean easy to use. The more elegant the implementation, the better. CPU-efficiency is not (that) important for me.

Dimmer answered 23/10, 2013 at 8:16 Comment(5)
I see nothing wrong with using enums. And it's easy to extend. (not exactly 3 valued logic) Where I work gender first was: "male,female" then became : "male,female,unknown" then became : "male,female,unknown,not applicable."Stretcherbearer
@PieterB ...but the two answers allow a more elegant usage. And I think it is more "generic" than male/female.Dimmer
Thank you very much for both answers, I wasn't aware that operator overloading is available in D2006 (but it is:). Both answers seem to provide the elegant solution I was looking for (+1). I'll tinker around a little bit and than I'll decide, which one to finally accept.Dimmer
I would put Unknown before True/False - if ever you forget to set the result of a value it will always default to the first value of the enum. The negative side of this is that the ordinal values will deviate from the Boolean enumVulgate
@MattAllwood Value types are not default initialised when they are local variables or return valuesJustly
J
15

You could implement an enhanced record with operator overloading. It would look like this:

type
  TTriBool = record
  public
    type
      TTriBoolEnum = (tbFalse, tbTrue, tbUnknown);
  public
    Value: TTriBoolEnum;
  public
    class operator Implicit(const Value: Boolean): TTriBool;
    class operator Implicit(const Value: TTriBoolEnum): TTriBool;
    class operator Implicit(const Value: TTriBool): TTriBoolEnum;
    class operator Equal(const lhs, rhs: TTriBool): Boolean;
    class operator LogicalOr(const lhs, rhs: TTriBool): TTriBool;
    function ToString: string;
  end;

class operator TTriBool.Implicit(const Value: Boolean): TTriBool;
begin
  if Value then
    Result.Value := tbTrue
  else
    Result.Value := tbFalse;
end;

class operator TTriBool.Implicit(const Value: TTriBoolEnum): TTriBool;
begin
  Result.Value := Value;
end;

class operator TTriBool.Implicit(const Value: TTriBool): TTriBoolEnum;
begin
  Result := Value.Value;
end;

class operator TTriBool.Equal(const lhs, rhs: TTriBool): Boolean;
begin
  Result := lhs.Value=rhs.Value;
end;

class operator TTriBool.LogicalOr(const lhs, rhs: TTriBool): TTriBool;
begin
  if (lhs.Value=tbTrue) or (rhs.Value=tbTrue) then
    Result := tbTrue
  else if (lhs.Value=tbFalse) and (rhs.Value=tbFalse) then
    Result := tbFalse
  else
    Result := tbUnknown;
end;

function TTriBool.ToString: string;
begin
  case Value of
  tbFalse:
    Result := 'False';
  tbTrue:
    Result := 'True';
  tbUnknown:
    Result := 'Unknown';
  end;
end;

Some sample usage:

var
  x: Double;
  tb1, tb2: TTriBool;

tb1 := True;
tb2 := x>3.0;
Writeln((tb1 or tb2).ToString);

tb1 := False;
tb2.Value := tbUnknown;
Writeln((tb1 or tb2).ToString);

which outputs:

True
Unknown
Justly answered 23/10, 2013 at 8:48 Comment(21)
won't Delphi complain on case Value of that not all values were covered in .ToString? at least XE2 tends to complain in this case and after some thought i agreed it was right.Bega
@Arioch'The Which value is not handled? What is the warning number that you are referring to?Justly
TTriBool.TTriBoolEnum(3), TTriBool.TTriBoolEnum(4), ... TTriBool.TTriBoolEnum(255) After hitting few uncovered cases after extending old code, i am inclined that the forward-compatibiltiy (or rather explicit forward incompatibility) is a good thing to have. Add there not-initialized variables as well. Yes, that is defensive programming, but TS said he was not very CPU-bound. Also i stated those concerns in the comments in my code.Bega
@Arioch'The Which warning number please?Justly
@Arioch'The My code doesn't have that warning because the return value is a string. Passed as a var parameter as per earlier discussions. And initialized since it is a managed type. Warning failure. In my own code I would always have an else clause to a helper function named RaiseAssertionFailed and I'd pass Result, and that would shut the compiler up.Justly
The idea to "shut compiler up" is the idea behind "try ... except end;" The question here is not to get rid of the warning, but to get rid of potential danger which the warning is only a symptom. And the lack of warning does not mean the code got not fragile, it only means Delphi could not determine that in this case. Surely YMMV in regard of how much defense should be in your code. Personally i think those patterns of code are rooks ont the running field for future maintainers. Again, YMMV.Bega
Would one expect a TTriBool variable to have status tbUnspecified even if it is not assigned a value?Affluence
@LURD Would one expect an integer to have any particular value if not assigned a value? One needs to initialise variables.Justly
The idea behind the else statement is to satisfy the compiler, and to defend against errors occurring in the future when the dev adds another enum value and forgets to update the case statement. My "shut the compiler up" was flippant. I do understand that one wants to treat the disease and not the symptom. And in my own code, all such case statements have an else clause. I personally have no fear of uninitialised variables and don't feel need to try to detect them in such an else clause. For me it's a given that the programmer must initialise variables.Justly
Programmer has to write code without errors, but... but it does not always happen. Same for variables, lackign init in some code paths. "And in my own code, all such case statements have an else clause" - and then you're posting on SO code that lacks the feature that you use yourself. Given that SO is somewhat didactical site, targeted at learning novices good practices, i think posting here code that is fragile without saying it is basically imposing dangerous practices among novices. Twice so when the question starts with "How to best implement" Okay, enough on this sub-topic.Bega
@Arioch'The You cannot detect an uninitialized value type variable at runtime. I think it's pointless to try to do so.Justly
For this particular datatype, you can do it with 253 to 3 probability. However nothing is totally bulletproof of course.Bega
@Arioch'The Under the assumption that the values of the uninitialized variable are uniformly distributed. Which is a very very bad assumption!!Justly
In D2006, I get E2003 Undefined identifier: 'TTriBoolEnum'. If I put the definition of TTriBoolEnum outside the record, everything works fine.Dimmer
@Dimmer I guess nested types came in after D2006.Justly
Single byte ? on some random place on stack? why ? Interesting idea, did not think of it though. PS: in case of global vars or object fields they would be implicitly initialized. So, well, really, i assumed that random local variably (or heap-allocated one) would have randomly chosen byte uniformly distributed. I agree that is absolutely not proved. Still i don't see the ubiquitous mechanics that would break that assumption. And if to account for defensive heap FreeMem implementation using patterns like 0xDEADBEEF then detectiob probability becomes even higher.Bega
If I put the definition of TTriBoolEnum don't thnk it is needed at all here. IT could be needed for compatibility with inherited code, but hardly for one written from scratch.Bega
I checked your example code. You got the logic of LogicalOr wrong. Have a look at en.wikipedia.org/wiki/Three-valued_logic. Maybe you confused with null. The idea is different here. Shall I edit your answer? Besides, to avoid confusion I would rather use tbUnknown than tbUnspecified.Dimmer
I fixed it now. I don't think it really matters. You aren't going to use my implementation I hope. You are just going to use the concepts. Anyway, you are correct that we ought to make the answer match the question!Justly
You are right, it doesn't really matter, I just didn't want to modify your answer without asking first. Anyway, thank you very much, I definitely got the idea and don't see a reason not to build on your implementation.Dimmer
In such a clear cut case like this, I'd have been very happy for you to make that edit. But thanks for asking!!Justly
C
5

AS. What did you mean by elegancre here ? Elegance of implementation or elegance of use or CPI-effieciency or maintainability ? Elegance is a very vague word...

I think the obvious way to make it easier to use is converting the type to be usable in the fashion like ExtBoolean1 or (ExtBoolean2 and True).

However the features required might be in or short before Delphi 2006 (quite a buggy release per se), so take your DUnit and do a lot of tests..

To list the features to be used and their descriptions:

  1. Enhanced Records: When should I use enhanced record types in Delphi instead of classes? and http://delphi.about.com/od/adptips2006/qt/newdelphirecord.htm and manual
  2. Operation overloading, including implicit typecasts: What operator do I overload when assigning an "Enhanced Record" to a normal "Data Type" variable? and Operator Overloading in Delphi and manual
  3. Functions inlining: what is use of inline keyword in delphi and manual

To outline some of those ideas:

type
  TExtBoolean = record
     Value: (ebUnknown, ebTrue, ebFalse);

     function IsNull: boolean; inline;
     function Defined: boolean; inline;

     class operator Implicit ( from: boolean ): TExtBoolean; inline;
     class operator Implicit ( from: TExtBoolean ): boolean; 
     class operator LogicalAnd( Value1, Value2: TExtBoolean ):   TExtBoolean; 
     class operator LogicalAnd( Value1: TExtBoolean; Value2: boolean):  TExtBoolean; inline;
     class operator LogicalAnd( Value1: boolean; Value2: TExtBoolean ):   TExtBoolean; 
....
  end;

const Unknown: TExtBoolean = (Value: ebUnknown); 

...
var v1: TExtBoolean;
    v1 := False; 
    v1 := True;
    v1 := Unknown;
...

class operator TExtBoolean.Implicit ( from: boolean ): TExtBoolean; 
begin
  if from
     then Result.Value := ebTrue
     else Result.Value := ebFalse
end;

class operator TExtBoolean.Implicit ( from: TExtBoolean ): Boolean; 
begin
  case from.Value of
    ebTrue: Result := True;
    ebFalse: Result := False;  
    else raise EConvertError.Create('....');
end;


function TExtBoolean.Defined: boolean; 
begin
  Result := (Self.Value = ebTrue) or (Self.Value = ebFalse);
end;

// this implementation detects values other than ebTrue/ebFalse/ebUnkonwn
// that might appear in reality due to non-initialized memory garbage 
// since hardware type of Value is byte and may be equal to 3, 4, ...255
function TExtBoolean.IsNull: boolean; 
begin
  Result := not Self.Defined
end;

class operator TExtBoolean.And( Value1, Value2: TExtBoolean ): TExtBoolean; 
begin
  if Value1.IsNull or Value2.IsNull
     then Result.Value := eb.Undefined
     else Result := boolean(Value1) and boolean(Value2);
// Or, sacrificing readability and safety for the sake of speed
// and removing duplicate IsNull checks
//   else Result := (Value1.Value = ebTrue) and (Value2.Value = ebTrue);
end;

class operator TExtBoolean.LogicalAnd( Value1, TExtBoolean; Value2: boolean):  TExtBoolean;
begin
  Result := Value2 and Value1;
end;

class operator TExtBoolean.LogicalAnd( Value1: boolean; Value2: TExtBoolean ):   TExtBoolean; 
begin
  if Value2.IsNull
     then Result := Value2
     else Result := Value1 and (Value2.Value = ebTrue);
// or if to accept a duplicate redundant check for readability sake
//   and to avert potential later erros (refactoring, you may accidentally remove the check above)
//    else Result := Value1 and boolean (Value2);
end;

etc

PS. The check for being unspecified above is intentionally made pessimistic, tending to err on bad side. It is the defense against non-initialized variables and possible future changes, adding more states than three. While thise might seems to be over-protecting, at least Delphi XE2 is agreeing with mee: see the warning in a similar case:

program Project20;  {$APPTYPE CONSOLE}
uses System.SysUtils;

type enum = (e1, e2, e3);
var e: enum;

function name( e: enum ): char;
begin
  case e of
    e1: Result := 'A';
    e2: Result := 'B';
    e3: Result := 'C';
  end;
end;

// [DCC Warning] Project20.dpr: W1035 Return value of function 'name' might be undefined

begin
  for e := e1 to e3
      do Writeln(name(e));
  ReadLn;
end.
Colin answered 23/10, 2013 at 8:36 Comment(2)
With elegance I mean easy to use. The more elegant the implementation, the better. CPU-efficiency is not (that) important for me.Dimmer
Thank you very much for your answer. Both answers are very similar. I accepted the other answer, because I only can accept one and the other code example is a little bit cleaner IMO.Dimmer

© 2022 - 2024 — McMap. All rights reserved.