Jump to page: 1 2
Thread overview
Use UFCS for reducing dependencies
Jul 16, 2022
Hipreme
Jul 17, 2022
Bastiaan Veelo
Jul 17, 2022
Paul Backus
Jul 18, 2022
Hipreme
Jul 18, 2022
Paul Backus
Jul 18, 2022
rikki cattermole
Jul 18, 2022
Paul Backus
Jul 18, 2022
rikki cattermole
Jul 18, 2022
Adam D Ruppe
Jul 17, 2022
Dave P.
Jul 17, 2022
Bastiaan Veelo
July 16, 2022

Old habits die hard.

If you're coding in Java, imagine that you want to read a PNG from a file, you'll basically have a class like:

class PNG
{
    public bool loadFromMemory(ubyte[] data){return decode(data);}
    public bool loadFromFile(string filePath){return readFromMemory(file.read(filePath));}
}

Usually it is very common adding API for loading files in your decoders, specially in high level wrappers, but this creates a problem: Decoding your file is totally unrelated to reading the file, yet, you did created the dependency between file and decoder.

After some refactoring in my project, trying to reduce dependencies, I came to a solution that we should not create dependencies like that.

Thanks to UFCS, we can extend the class without actually creating such dependency, even in high level wrappers, the way to extend your class without creating that kind of dependency is by basically creating a extension module:

module png.extension;

//This module contain png extensions based on other libraries.

version(FileSystemPNG)
bool loadFromFile(PNG png, string filePath)
{
    return png.loadFromMemory(file.ready(filePath));
}

That way, one could easily call it like:

import png.extension;

//One could even create a file which would import both png.decoder and png.extension

PNG png = new PNG();
png.loadFromFile("somewhere.png");

You could even save the path inside the PNG at the constructor and .loadFromFile would directly access its member.

I came here to show this technique because often people will try coding D without really thinking the D way to solve problems, as it happened to me. The code become a lot cleaner, one less function to worry in your class, one less dependency. I have been doing a real refactor on my code around that concept, that way, one could easily use any kind of file system reading without even needing to refactor the code. Even better, you could create your own extension without needing to modify the PNG code.

This is, together with the Range interfaces, one of really valid and useful usecase for UFCS. If you guys have any other techniques you use for reducing dependencies, I would be glad to know :)

July 17, 2022

On Saturday, 16 July 2022 at 22:10:13 UTC, Hipreme wrote:

>

Old habits die hard.

[...]

>

After some refactoring in my project, trying to reduce dependencies, I came to a solution that we should not create dependencies like that.

Instead of using versions and sub-modules, using templates and conditional compilation is probably easier:

struct PNG
{
    this(T)(T source)
    {
        static if (is(T == string))
        {
            pragma(msg, "conditional dependency on stdio");
            import std.stdio;

            // Load from file
        }
        else static if (is(T == ubyte[]))
        {
            // Decode from memory
        }
        else
            static assert(false, "Unsupported source type " ~ T.stringof);
    }
}

void main()
{
    // auto png = PNG("somewhere.png");
    auto png = PNG(new ubyte[10]);
}

Only the code that corresponds to the type that the constructor is called with gets compiled in, including any imports that are there. So the import of std.stdio only happens if a PNG is read from file.

Another tip: unless you need polymorphism or reference behaviour for your type, a struct is often preferred over a class. Old habits die hard :-)

-- Bastiaan.

July 17, 2022

On Sunday, 17 July 2022 at 12:08:38 UTC, Bastiaan Veelo wrote:

>

On Saturday, 16 July 2022 at 22:10:13 UTC, Hipreme wrote:

>

Old habits die hard.

[...]

>

After some refactoring in my project, trying to reduce dependencies, I came to a solution that we should not create dependencies like that.

Instead of using versions and sub-modules, using templates and conditional compilation is probably easier:

struct PNG
{
    this(T)(T source)
    {
        static if (is(T == string))
        {
            pragma(msg, "conditional dependency on stdio");
            import std.stdio;

            // Load from file
        }
        else static if (is(T == ubyte[]))
        {
            // Decode from memory
        }
        else
            static assert(false, "Unsupported source type " ~ T.stringof);
    }
}

Another possibility:

struct PNG
{
    static PNG load(Source)(Source source)
        if (isInputRange!Source && is(ElementType!Source == ubyte))
    {
        // etc.
    }
}

This way, the PNG module itself is completely agnostic about what data source it loads from, and has no explicit dependencies (except on std.range, I guess).

July 17, 2022

On Saturday, 16 July 2022 at 22:10:13 UTC, Hipreme wrote:

>

[...]

I like this. What's annoying is that it doesn't work smoothly with structs. Methods automatically deference pointers, but UFCS functions don't.

struct Foo {
    int x;
    void mutate(){
        x++;
    }
}

void mutate2(ref Foo foo){
    foo.x++;
}

void main(){
    Foo foo;
    Foo* pfoo = &foo;

    // Both ref and pointer work for methods
    foo.mutate();
    pfoo.mutate();

    // Ref works for ufcs
    foo.mutate2();
    // But pointer doesn't work for UFCS

    // Can't do this:
    // pfoo.mutate2();

    // Must do this:
    (*pfoo).mutate2();

    assert(foo.x == 4);
}

July 17, 2022

On Sunday, 17 July 2022 at 16:25:52 UTC, Dave P. wrote:

>

On Saturday, 16 July 2022 at 22:10:13 UTC, Hipreme wrote:

>

[...]

I like this. What's annoying is that it doesn't work smoothly with structs. Methods automatically deference pointers, but UFCS functions don't.

struct Foo {
    int x;
    void mutate(){
        x++;
    }
}

void mutate2(ref Foo foo){
    foo.x++;
}

void main(){
    Foo foo;
    Foo* pfoo = &foo;

    // Both ref and pointer work for methods
    foo.mutate();
    pfoo.mutate();

    // Ref works for ufcs
    foo.mutate2();
    // But pointer doesn't work for UFCS

    // Can't do this:
    // pfoo.mutate2();

    // Must do this:
    (*pfoo).mutate2();

    assert(foo.x == 4);
}

It works if you add an overload:

void mutate2(Foo *foo)
{
    mutate2(*foo);
}
July 18, 2022

On Sunday, 17 July 2022 at 16:15:07 UTC, Paul Backus wrote:

>

Another possibility:

struct PNG
{
    static PNG load(Source)(Source source)
        if (isInputRange!Source && is(ElementType!Source == ubyte))
    {
        // etc.
    }
}

This way, the PNG module itself is completely agnostic about what data source it loads from, and has no explicit dependencies (except on std.range, I guess).

Could you extend it a bit further how would that work?

July 18, 2022

On Monday, 18 July 2022 at 10:40:23 UTC, Hipreme wrote:

>

On Sunday, 17 July 2022 at 16:15:07 UTC, Paul Backus wrote:

>

Another possibility:

struct PNG
{
    static PNG load(Source)(Source source)
        if (isInputRange!Source && is(ElementType!Source == ubyte))
    {
        // etc.
    }
}

This way, the PNG module itself is completely agnostic about what data source it loads from, and has no explicit dependencies (except on std.range, I guess).

Could you extend it a bit further how would that work?

Well, I don't know the algorithm for PNG decoding, so I don't know whether it requires an input range, a forward range, or a random access range. But the basic idea is, you declare your load function as taking a generic range type, and then you can load from anything that implements the appropriate range interface.

Because the algorithm (PNG.load) and the data sources communicate only via the range API, neither one needs to have any special knowledge of, or explicit dependency on, the other.

This is the fundamental idea behind ranges. If you have N data sources that implement the range interface, and M algorithms that consume the range interface, then you do not need to implement all M×N combinations by hand--all of your data sources and all of your algorithms will Just Work™ with each other.

July 19, 2022
On 19/07/2022 12:46 AM, Paul Backus wrote:
> Well, I don't know the algorithm for PNG decoding, so I don't know whether it requires an input range, a forward range, or a random access range.

For AV handling, ranges are not the right tool for the job, too inefficient.

You want to be working with arrays directly.
July 18, 2022
On Monday, 18 July 2022 at 12:46:20 UTC, Paul Backus wrote:
> But the basic idea is, you declare your `load` function as taking a generic range type, and then you can load from anything that implements the appropriate range interface.

https://github.com/adamdruppe/arsd/blob/master/png.d#L1145

This was actually one of the first times I tried to write a range consumer, so.... not very good code. (Hit the git blame button and find that whole block was committed in June 2013... before I wrote the range chapter of my book lol)

But still, it shows the rough concept in a real implementation.
July 18, 2022
On Monday, 18 July 2022 at 12:55:39 UTC, rikki cattermole wrote:
>
> On 19/07/2022 12:46 AM, Paul Backus wrote:
>> Well, I don't know the algorithm for PNG decoding, so I don't know whether it requires an input range, a forward range, or a random access range.
>
> For AV handling, ranges are not the right tool for the job, too inefficient.
>
> You want to be working with arrays directly.

You can have a fast path for arrays and fall back to the slower, more generic version for other ranges.

But yes, the whole point of ranges is to decouple your algorithm from the source of the data it operates on. If you *want* your algorithm to be coupled to a particular source of data, then ranges will only get in your way. (Although it may be worth asking yourself: are you really sure that's what you want?)
« First   ‹ Prev
1 2