Recent

Author Topic: Custom type for Nil  (Read 4480 times)

PascalDragon

  • Hero Member
  • *****
  • Posts: 4761
  • Compiler Developer
Re: Custom type for Nil
« Reply #45 on: March 17, 2022, 01:07:27 pm »
In my opinion lambda A, B as A.ID - B.ID is more Pascal (and that is a syntax I had already thought about adding once anonymous functions are ready, but other devs don't agree here). However the problem is that this requires type inference and that is a concept that as such does not exist in Pascal or the compiler, thus making this a rather complex functionality.
Already thought that this might pose a problem, but such a feature might also be useful for generics, to deduce generic types:
Code: Pascal  [Select][+][-]
  1. generic function foo<T>(val: T);
  2. ...
  3.  
  4. foo<>(24); // instead of foo<Integer>(24)

This - which is called implicit specializations - is already in development (the syntax is without any <> (or specialize) at all, cause the compiler might pick a non-generic routine as well) and is much less of a problem then the type inference required for such lambda expressions. This is because for a lambda expression the compiler would essentially need to parse the whole thing blindly, then determine which function is called (if the lambda is a parameter there might be multiple suitable overloads after all) and then it would need to recheck the lambda to determine whether the generated code would fit. For an implicit specialization the compiler only needs to check whether the given parameters would satisfy any of the suitable generic routines (or maybe even a non-generic).

Also I don't know whether this would be some code I'd like to read:
Code: Pascal  [Select][+][-]
  1. TList<Integer>.Combine(list1, list2, +);
  2. // or
  3. specialize TList<Integer>.Combine(list1, list2, @+);
I thought more about something like
Code: Pascal  [Select][+][-]
  1. TList<Integer>.Combine(list1, list2, operator Integer.Add);
  2. // or
  3. TList<Integer>.Combine(list1, list2, operator Add);
Or something similar. I actually like wordy syntax more than just throwing symbols there, and at least for mode delphi the operators already have names (this would also make disambigouagtion between binary and unary operator - easier)

In mode ObjFPC operators are not named, they are symbols. But I agree in so far that a keyword operator might make things look nicer. In mode ObjFPC:

Code: Pascal  [Select][+][-]
  1. specialize TList<Integer>.Combine(list1, list2, @operator +);

However consistency would also dictate then that code like this is possible:

Code: Pascal  [Select][+][-]
  1. var
  2.   i1, i2, i3: LongInt;
  3. begin
  4.   i3 := operator +(i1, i2);
  5. end.

Warfley

  • Hero Member
  • *****
  • Posts: 942
Re: Custom type for Nil
« Reply #46 on: March 17, 2022, 02:52:32 pm »
Code: Pascal  [Select][+][-]
  1. specialize TList<Integer>.Combine(list1, list2, @operator +);
I think this looks neat tbh
This - which is called implicit specializations - is already in development (the syntax is without any <> (or specialize) at all, cause the compiler might pick a non-generic routine as well) and is much less of a problem then the type inference required for such lambda expressions. This is because for a lambda expression the compiler would essentially need to parse the whole thing blindly, then determine which function is called (if the lambda is a parameter there might be multiple suitable overloads after all) and then it would need to recheck the lambda to determine whether the generated code would fit. For an implicit specialization the compiler only needs to check whether the given parameters would satisfy any of the suitable generic routines (or maybe even a non-generic).
With regards to this, and this also ties in with the restrictions for generics discussed earlier in this thread, for some time I now had one thing in mind that kinda annoyed me a little on the usability of generics, and that is the distinction between classes, objects and records.

Because with a lot of use-cases you do not need to care if your generic argument is an advanced record, object or class, as long as it provides the suitable interface:
Code: Pascal  [Select][+][-]
  1. generic procedure WriteData<T>(constref AData: T);
  2. begin
  3.   WriteLn(AData.ToString);
  4. end;
This works for any Type that provides .ToString. But there is one big difference, that is if you transfer ownership the the generic object or function, it needs to know if it has to call Free, done or simply let the finalisation of the compiler do the rest. Usually I solve this with another additional generic parameter:
Code: Pascal  [Select][+][-]
  1. generic procedure WriteDataAndFree<T, TFinalizer>(constref AData: T);
  2. begin
  3.   WriteLN(AData.ToString);
  4.   TFinalizer.FinalizeData(AData);
  5. end;
With three "partial" specializations:
Code: Pascal  [Select][+][-]
  1. generic procedure WriteDataAndFreeClass<T>(constref AData: T);
  2. begin
  3.   specialize WriteDataAndFree<T, specialize TClassFinalizer<T>>(AData); // where TClassFinalizer.FinalizeData calls Free on T
  4. end;
  5. generic procedure WriteDataAndFreeObject<T>(constref AData: T);
  6. begin
  7.   specialize WriteDataAndFree<T, specialize TObjectFinalizer<T>>(AData); // where TObjectFinalizer.FinalizeData calls Done on T
  8. end;
  9. generic procedure WriteDataAndFreeRecord<T>(constref AData: T);
  10. begin
  11.   specialize WriteDataAndFree<T, TNoopFinalizer>(AData); // where TNoopFinalizer.FinalizeData does nothing as the compiler will add the finalization code
  12. end;
What would be much nicer would be the following:
Code: Pascal  [Select][+][-]
  1. generic procedure WriteDataAndFree<T: TObject>(constref AData: T);
  2. begin
  3.   specialize WriteDataAndFree<T, specialize TClassFinalizer<T>>(AData); // where TClassFinalizer.FinalizeData calls Free on T
  4. end;
  5. generic procedure WriteDataAndFree<T: object>(constref AData: T);
  6. begin
  7.   specialize WriteDataAndFree<T, specialize TObjectFinalizer<T>>(AData); // where TObjectFinalizer.FinalizeData calls Done on T
  8. end;
  9. generic procedure WriteDataAndFree<T: Record>(constref AData: T);
  10. begin
  11.   specialize WriteDataAndFree<T, TNoopFinalizer>(AData); // where TNoopFinalizer.FinalizeData does nothing as the compiler will add the finalization code
  12. end;
And letting the compiler figure out what to call. One area in particular is for enumerators, because there the programmer does probably not even know what kind of enumerator there is (most enumerators encountered in the wild are classes, but I have already seen and made records being enumerators)
So considering the following map function:
Code: Pascal  [Select][+][-]
  1. generic function Map<TFrom, TTo, TEnumerator>(const Enumerator: TEnumerator; MapFunction: specialize TMapFunction<TFrom, TTo>): specialize TMappingIterator<TFrom, TTo>;
  2. begin
  3.   Result := specialize TMappingIteratorClass<TFrom, TTo, TEnumerator>.Create(Enumerator, MapFunction); // inherits from TMappingIterator to be applicable to a specific enumerator
  4. end;
  5.  
  6. // usage with implicit specialization
  7. for str in Map(MyIntegerList.GetEnumerator, @IntToStr) do
  8.   ...
Where TMappingIterator would be an iterable object (i.e. returns Self on GetEnumerator and has MoveNext and Current), and would apply map to each element. Currently this would need 3 functions MapObject, MapClass and MapRecord, which generally is not that bad (which calls the corresponding TMapIterator for either Obejcts, Classes or Records), but the user needs to know what kind of type the Enumerator is, which is somethign I usually do not know for most collections.
Also something as described above would allow this:
Code: Pascal  [Select][+][-]
  1. generic function MapEnumerator<TFrom, TTo, TEnumerator: TObject>(const Enumerator: TEnumerator; MapFunction: specialize TMapFunction<TFrom, TTo>): specialize TMappingIterator<TFrom, TTo>;
  2. generic function MapEnumerator<TFrom, TTo, TEnumerator: Object>(const Enumerator: TEnumerator; MapFunction: specialize TMapFunction<TFrom, TTo>): specialize TMappingIterator<TFrom, TTo>;
  3. generic function MapEnumerator<TFrom, TTo, TEnumerator: Record>(const Enumerator: TEnumerator; MapFunction: specialize TMapFunction<TFrom, TTo>): specialize TMappingIterator<TFrom, TTo>;
  4.  
  5. generic function Map<TFrom, TTo, TCollection>(const Collection: TCollection; MapFunction: specialize TMapFunction<TFrom, TTo>): TMappingIterator<TFrom, TTo>;
  6. begin
  7.   MapEnumerator(Collection.GetEnumerator, MapFunction);
  8. end;
Which would allow the following construct:
Code: Pascal  [Select][+][-]
  1. for str in Map(MyIntegerList, @IntToStr) do
  2.   ...
Working for every kind of collection, no matter how they implement their enumerator. Which I think would be really neat.

Also nice would be to also have a "fallback" declaration:
Code: Pascal  [Select][+][-]
  1. generic procedure Test<T: TObject>; // chosen for all classes
  2. generic procedure Test<T: Object>; // chosen for all objects
  3. generic procedure Test<T>; // chosen if nothing of the above matched
  4.  
But this is basically C++ SFINAE, which, while the idea is nice, devolved through the use of the community to one of the most confusing paradigms I have ever seen in software development
« Last Edit: March 17, 2022, 04:27:28 pm by Warfley »

 

TinyPortal © 2005-2018