Fluent coding - debugging

I don’t know if this is old and/or obvious, but I’ve read a bit about fluent-style Delphi over the years, and people have mentioned debugging in the IDE as one issue.

Having been getting a bit of a functional programming perspective this year, I had an idea about how it might work to apply the identity transformation … ie a ‘function’ (or whatever) that returns you exactly what you give it.

I didn’t have time tonight to look at these:

" Fluent interfaces are used in e.g. TStringBuilder, THTMLWriter and TGpFluentXMLBuilder."

So I dug up some old code that was offered in a previous discussion (2011 !).

I stuck the code here: GitHub - pmcgee69/Fluent-ID-example: Example of debugging fluent style code in Delphi using the identity transformation.

and captured a 2 min video of using it to step through code :

The ultimate purpose was just processing ideas - ie using an “id” function (or you could call it “brk”) and a single break point as a way to place a debug point or points into a fluent sequence.

With that sort of code I find myself repeatedly doing a cycle of F7 followed by Shift+F8 to reach the point I actually want to debug.

Or I use some local variables temporarily so I don’t go insane trying to keep track of which part of the fluent call I’m executing.

Debugging fluent api’s is not fun in any language - but for the most part you just use them and let the library authors debug them :wink: It’s not a pattern that I would recommend you adopt for every day line of business code.

Delphi Mocks uses a fluent interface, but in that situation the type each call returns is not always the same - doing this

//SETUP: Add a check that SimpleMethod is called atleast once.
  mock.Setup.Expect.AtLeastOnce.When.SimpleMethod;

in a non fluent manner would require a bunch of variables and make the code extremely verbose. So in this scenario it makes sense.

Seeing whether it was possible at all was a first goal.
I need to look into how it might interact with pre-existing classes / interfaces.

My next goal is to see how extra info (like a fail / quit condition) can also be passed along the sequence, and what steps can be taken to short-circuit the sequence if that occurs.

Any info on existing approaches? I will go look at the code from gabr + vincent + others.

The fluent paradigm requires that a call returns a fixed type - my guess would be if it fails the only way to let the use know would be to raise an exception.

Having a fixed type that contains possible failure info doesn’t conflict with the idea of a “Maybe” datatype that can encompass both (X and Nothing).

That’s something I have in mind, but I certainly don’t know enough yet to say if it could become something workable.

It might eg work when you are the one writing the classes / objects / interfaces. Eg the TFluentObj and TFluentIntf from my example could carry a boolean property, I think.

But I assume you might meet a problem when you want to use a ToString or whatever from other code in the sequence. :thinking:

From a design perspective, any failure should generate an exception. Otherwise you are “bastardising” the elegant design :wink:

Exceptions are regarded in some circles as expensive, and there are applications where they can’t be used. This is the influence some of the c++ world is having on me.

But also, I think this comes from not traditionally having Maybe / Option / Nullable / Either types in Delphi (at least referred to in that way).

We are just talking about composing functions with identical signatures (monoids and monads) - and other languages are able to short circuit within that framework, so it’s something I’m trying to think about.

In delphi it’s more the try/except that is expensive.

The issue is how do you check for an error in a call chain

One.Two.Three.Four

if Two fails… what should happen?

1 Like

I think trying to shoehorn functional programming into delphi will be difficult, just as it is in c# (although easier). If you look at functional languages, they all have features explicitly designed to enable functional programming.

I’ve tried to bend delphi in many weird and wonderfull ways just to try and port c# - with some successes but mostly failures (often due to poor generics implementation).

1 Like

I’m interested to find out more about what happens in Delphi when exceptions occur.

Back in April (2021), prompted from both the c++ direction, and learning about Maybe types, I ran some simple measurements with division - either avoiding /0 or throwing, just to get an idea of the impact.

https:// github.com/ pmcgee69/Exceptions-Cost-in-Delphi

I’m sure it’s pretty rough, but I put this together as a very simple idea of how it might work.
I’m sure it could be done much more elegantly.
(At a much higher level, I’ve been having a look at Primož 's Pipelines)

   var _is : Internetstring;

   if not _is.clear(Fail)
             .get_res_name('google.com')
             .get_res_type('source')
             .get_res_next('cdn')
             .print
   then  err('data 1 retrieval error');                //  aaa.bbb.ccc

   if not _is.clear(Fail)
             .get_res_name('apple.com')
             .get_res_type('')
             .get_res_next('cdn')
             .print
   then  err('data 2 retrieval error');                //  data 2 retrieval error

          _is.clear(Succeed)
             .get_res_name('hp.com')
             .get_res_type('')
             .get_res_next('cdn')
             .print;                                   // aaa.ccc

Our standard language is, roughly speaking, functions and data types.
I think there are going to be Functional ideas that might change how we understand those things and influence how we use them - as opposed to changing the language per se.

My weekend … making an experimental mess, and slowly climbing out / cleaning up. :slight_smile:

To do some performance testing, I changed my example code to generate 10m strings of length = 8, drawn from base-21 characters. Then methods to trim the string to length = 4, and try to interpret that as hex, and save the result.

I wanted to try out TVirtualMethodInterceptor … which requires virtual methods. I had started with records, so measured perf, changed to classes, measured again; changed to virtual methods, measured again; and then started experimenting with the TVMI.

Creating, using, and destroying, VMI x 10m slowed things down TWENTY times.
But using a single VMI over and over cost basically nothing.

Trying exceptions to short-circuit was FIFTY times slower than using an error flag.
Next aim is to use the VMI OnBefore method to do the error check so it doesn’t have to be in each of the class methods.

(I also finally realised how to define constant records)

//Unit vector constants
const
  iHat : TVector3D = (x: 1; y: 0; z: 0);
  jHat : TVector3D = (x: 0; y: 1; z: 0);
  kHat : TVector3D = (x: 0; y: 0; z: 1);

1 Like
   Timer( procedure begin  for var i := 1 to arr_size do begin
                               try
                                 arr2[i] := arr[i].trunc(4).hextoint.int;
                               except
                                 arr2[i] := fail_int;
                               end;
                           end;
                    end );

   quickcheck(arr, arr2);   writeln('-');   cleararray(arr, arr2, arr_size);
   quickcheck(arr, arr2);

   var vmi := TVirtualMethodInterceptor.Create(Transformstring);
   (*
   vmi.OnBefore := procedure(  Instance  : TObject;        Method: TRttiMethod;
                               const Args: TArray<TValue>; out DoInvoke: Boolean;  out Result: TValue     )
                      begin
                        //Write('[before] ', Method.Name,'  ');
                      end;
   *)
   X := Transformstring.Create;
   vmi.Proxify( X );

   Timer( procedure
          begin  for var i := 1 to arr_size do  begin
                         X := arr[i];
                         arr2[i] := X.trunc(4).hextoint.int;
                 end;
          end );
   vmi.Free;
   quickcheck(arr, arr2);   writeln('-');

(Changing the error rate per character from 4-in-20 to 4-in-100, turns the numbers into ~100ms with error flags, ~1300ms with exceptions).