Jump to page: 1 25  
Page
Thread overview
December 03
Maybe someone has a pattern for doing this kind of thing...

So, I have a function that does something to a various set of things; let's say we're going to serialise them or something:

```
module serialise;

void serialise(T)(void[] buffer, T)
  if (isSomeInt!T)
{ /* serialise integer */ }

void serialise(void[] buffer, const(char)[] str)
{ /* serialise string */ }

// ...etc
```

And some serialiser somewhere calls `serialise(thing)` for each thing, where the type of thing chooses the right overload and we're all good... sure, everyone knows this pattern.

So, I add a user thing in a module somewhere:

```
module app.user_thing:

struct UserThing
{ ... }

void serialise(void[] buffer, ref UserThing t)
{ /* serialise UserThing */ }
```

Now this thing wants to be serialisable, so you implement a serialise
function beside it...
In C++, this works; because ADL (argument dependent lookup) will cause to
additionally search the scope where the argument is defined for overloads.
Trouble is, in D unless `app.user_thing` was imported inside the serialiser
where it makes the call to `serialise()`, this overload won't be found,
because the UserThing overload is not in scope for the serialiser.

I tried to simulate something like ADL by getting `__traits(parent, value)` in a loop until I find the module for non-builtin objects, and then have the serialiser import that module prior to the call, to attempt to make sure the argument's module is also in scope so any potential overloads can be found when it tries to make the call:

import default_serialise : serialise;

void doSerialise(Things...)(void[] buffer, Things things)
{
  static foreach (thing; things)
  {{
    static if (isUserType!thing)
    {
      enum thingMod = getModuleForThing!thing;
      mixin(import " ~  thingMod ~ ";");
    }
    serialise(buffer thing);
  }}
}

The surprise is that if `thingMod` has a symbol `serialise`, it imports it at the inner scope, and it shadows the global overload set rather than complementing it...

So, I guess the thing I'm stuck on is; given there are some imports at global scope, and it may have an overload set for some function; HOW can I import more items to that overload set?

I tried this, but it doesn't work:

import default_serialise : serialise;

void doSerialise(Things...)(void[] buffer, Things things)
{
  static foreach (thing; things)
  {{
    static if (isUserType!thing)
    {
      enum thingMod = getModuleForThing!thing;
      mixin(import " ~  thingMod ~ ";");
      import default_serialise : serialise; // re-import at inner scope,
beside the other one
    }
    serialise(buffer thing);
  }}
}

Re-importing the globals into the same scope doesn't cause them to combine either; just the symbol from whichever import statement appears first is the winner...

Has anyone ever experimented with a pattern like this? Essentially, I can't work out how to expand /combine an overload set to include symbols from multiple imports....


December 03
On Tuesday, 3 December 2024 at 11:55:50 UTC, Manu wrote:

> Re-importing the globals into the same scope doesn't cause them to combine either; just the symbol from whichever import statement appears first is the winner...
>
> Has anyone ever experimented with a pattern like this? Essentially, I can't work out how to expand /combine an overload set to include symbols from multiple imports....

You could merge overloads like this:

```
module a;

struct A
{
}

void serialise(void[], A)
{
    import std.stdio;
    writeln("A.serialize");
}

---
module b;

struct B
{
}

void serialise(void[], B)
{
    import std.stdio;
    writeln("B.serialise");
}

---
module default_serialise;

void serialise(T)(void[], T t)
{
    import std.stdio;
    writeln("T.serialise");
}

---
module main;

enum isUserType(T) = true; // change to whatever

// this is needed because we still cannot express "local to static foreach".
template serialiseOf(T)
{
    static if (is(__traits(parent, T) == module) && isUserType!T)
    {
        alias mod = __traits(parent, T);
        alias serialiseOf = mod.serialise;
    }
    else
    {
        static import default_serialise;
        alias serialiseOf = default_serialise.serialise;
    }
}

// this is needed because D doesn't allow overloading local functions.
template doSerialiseImpl(Things...)
{
    static foreach(Thing; Things)
        alias doSerialiseImpl = serialiseOf!Thing;
}

void doSerialise(Things...)(void[] buffer, Things things)
{
    static foreach (thing; things)
        doSerialiseImpl!Things(buffer, thing);
}

void main()
{
    import a, b;
    doSerialise(null, A(), B(), 42);
}
```

It never ceases to amaze me how difficult it still is to make such trivial things work.
December 03
On Tuesday, 3 December 2024 at 15:17:47 UTC, Max Samukha wrote:


>         static import default_serialise;
>         alias serialiseOf = default_serialise.serialise;


The static import is a leftover from a previous iteration. You should not need it.


December 03
On Tuesday, 3 December 2024 at 11:55:50 UTC, Manu wrote:
> Has anyone ever experimented with a pattern like this? Essentially, I can't work out how to expand /combine an overload set to include symbols from multiple imports....

I gave this go using an eponymous template - it's definitely a bit yucky, but seems to do as you want (would obviously need more extensive work for real use cases rather than this simple POC):

## serialise.d

```d
module serialise;

import std.traits : isNumeric;

string serialise(T)(T foo)
if(isNumeric!T)
{
    import std.conv : to;
    return foo.to!string();
}

string serialise(string s)
{
    return s;
}

string doSerialise(Things...)(Things things)
{
    string slowBuilder;

    static foreach (i, Thing; Things)
    {{
        enum hasSerialiser = __traits(compiles, serialiserFor!Thing);
        static if(hasSerialiser)
            slowBuilder ~= serialiserFor!Thing(things[i]);
        else
            slowBuilder ~= serialise(things[i]);
    }}

    return slowBuilder;
}

template serialiserFor(Thing)
{
    import std.traits : fullyQualifiedName;
    mixin("import thingMod = "~fullyQualifiedName!(__traits(parent, Thing))~";");
    alias serialiserFor = thingMod.serialise;
}
```

## app.d

```d
module app;

import std.stdio;

void main()
{
	import serialise;
	import std : writeln;

	writeln(doSerialise(UserThing("a")));
	writeln(doSerialise("a"));
}

struct UserThing
{
	string a;
}

string serialise(UserThing thing)
{
	return "UserThing: " ~ thing.a;
}
```

## Result

```
UserThing: a
a
```

Namely:

The use of an eponymous template helps avoid the symbol resolution issue - you could also try to mixin a local import like `import userthing : userThingSerialise = serialise` instead, but a template might be cleaner.

I use a `__traits(compiles)` ~~abuse~~ check since it's simple, but you can probably also do some stuff with [__traits(hasMember)](https://dlang.org/spec/traits.html#hasMember), `__traits(getMember)`, etc.

Another potential way if keeping the serialise function separate from the actual type is mandatory, which I can't be bothered to prototype, is to make a template struct similar to this:

```d
struct Serialiser(UserThingT, alias SerialiserFunc){...}
```

And then have a specific overload within the main serialiser module to handle this case:

```d
// Something like this at least.
yada serialise(Thing)(...)
if(isInstanceOf!(Serialiser, Thing))
{
  // Use SerialiserFunc against Thing?
}
```

Though there's probably a bunch of issues with lifetimes, const, ref, etc. with that approach.

Definitely an interesting issue, though personally I'd try my best to allow types to have a `serialise` function directly be a part of them rather than using free standing extentions.
December 03
On Tuesday, 3 December 2024 at 11:55:50 UTC, Manu wrote:

>
> import default_serialise : serialise;
>
> void doSerialise(Things...)(void[] buffer, Things things)
> {
>   static foreach (thing; things)
>   {{
>     static if (isUserType!thing)
>     {
>       enum thingMod = getModuleForThing!thing;
>       mixin(import " ~  thingMod ~ ";");
>     }
>     serialise(buffer thing);
>   }}
> }

BTW, do you really need to create the overload set? You could call the right `serialize` directly if one exists in the type's module and fall back to the default one if it doesn't.
December 04
This won't help you today but:

1. We are considering giving structs inheritance as part of replacing alias this.

2. I want a way to mark a method as 'reinterpreted' by the child class/struct. Specifically for serialization, removing the need for doing any lookups like this.

December 03
On Tuesday, 3 December 2024 at 17:35:36 UTC, Richard (Rikki) Andrew Cattermole wrote:
> This won't help you today but:
>
> 1. We are considering giving structs inheritance as part of replacing alias this.
>
> 2. I want a way to mark a method as 'reinterpreted' by the child class/struct. Specifically for serialization, removing the need for doing any lookups like this.

Please no.  Than then also makes the betterC subset too complex.

What facility of 'alias this' is it intended to preserve?  If only the implicit conversion, then a new form of operator, or special named member function strikes me as more suitable.

From my perspective, one the the nice things about the class / strict difference is that struct does not have any classful behaviour, i.e. it has no inheritance.  If you add inheritance, that adds a thinking burden when reading code, and essentially removes the difference between class and struct, other than the implicit lock within classes.  I view the 'reference type' thing as a rather trivial difference.

One could add something like the struct embedding of Go/Limbo/Alef/Ken-C together with its implicit conversion for methods/functions.  The method is then interpreted by the embedded element, but against the embedded element, not the parent one.

December 04
On 04/12/2024 7:22 AM, Derek Fawcus wrote:
> On Tuesday, 3 December 2024 at 17:35:36 UTC, Richard (Rikki) Andrew Cattermole wrote:
>> This won't help you today but:
>>
>> 1. We are considering giving structs inheritance as part of replacing alias this.
>>
>> 2. I want a way to mark a method as 'reinterpreted' by the child class/struct. Specifically for serialization, removing the need for doing any lookups like this.
> 
> Please no.  Than then also makes the betterC subset too complex.
> 
> What facility of 'alias this' is it intended to preserve?  If only the implicit conversion, then a new form of operator, or special named member function strikes me as more suitable.
> 
>  From my perspective, one the the nice things about the class / strict difference is that struct does not have any classful behaviour, i.e. it has no inheritance.  If you add inheritance, that adds a thinking burden when reading code, and essentially removes the difference between class and struct, other than the implicit lock within classes.  I view the 'reference type' thing as a rather trivial difference.
> 
> One could add something like the struct embedding of Go/Limbo/Alef/Ken-C together with its implicit conversion for methods/functions.  The method is then interpreted by the embedded element, but against the embedded element, not the parent one.

My ideas post: https://forum.dlang.org/post/llqcjziyurwmyhzseonm@forum.dlang.org

The problem is alias this is too complex, and has introduced bugs that cannot be fixed, as it is relied upon.

Struct inheritance alone cannot fix it (note I did not originally propose this, that was Walter), my proposal is to add a way to reparent the parent most type, to replicate a lot of use cases of alias this without the bad behavior.

As a follow user of -betterC, I want to get full D classes working in it. Because there is absolutely no reason why they should not work (although such things as Object root class would not be present, therefore would need to be explicit).

This may seem unnecessary, but removing the coupling between the language implementation and druntime is important for pay as you go users, and with that custom runtimes. Which offers us greater portability.

December 03
On Tuesday, 3 December 2024 at 11:55:50 UTC, Manu wrote:
> Has anyone ever experimented with a pattern like this? Essentially, I can't work out how to expand /combine an overload set to include symbols from multiple imports....

as a compromise you could have an uda that packs custom serialisation defined on type itself or field of that type, then you can make serializer employ it when it sees it somewhere.

Best regards,
Alexandru.
December 03
Koenig lookup? I ran away, far away, from that, after implementing it for C++.

It's a nightmare.
« First   ‹ Prev
1 2 3 4 5