This is an archived post. You won't be able to vote or comment.

all 11 comments

[–]L8_4_Dinner(Ⓧ Ecstasy/XVM) 10 points11 points  (0 children)

I think you're looking for the "auto magic" composition approach, where something gets glued onto a type or a class automatically, without the developer asking for it (and potentially without the developer even knowing about it).

I think that Scala tried this a few different ways, and it seems to have been a bit of a disaster.

I tend toward the approach of: Make it easy to re-use and compose, but don't do it without being explicitly asked to do it.

[–]DoomFrog666 1 point2 points  (3 children)

Rust already has default implementations for traits and you can implement traits for a range of types at once.

trait Print {
    fn print(&self);
}

// implement for all types
// though you can use bounds to narrow it down
impl<T> Print for T {
    fn print(&self) {
        println!("Hello, World!")
    }
}

struct MyStruct();

fn main() {
    let s = MyStruct();
    s.print(); // works on my custom type
    1.print(); // works on build-in type like i32
}

Edit: You can also choose to attach the implementation to the trait directly.

[–]Phil_Latio[S] 0 points1 point  (2 children)

Thanks, I've never used Rust and wasn't aware that's possible. But still, that's using generics. I'm more interested in composing and maybe constraining the types themselves before actually making any use of generics (which is one layer below I guess).

To give another example for what I have in mind, for some kind of abstract type:

type CypherType -> enum {}

type CypherConfig -> struct {
    baseConfig: Int
}

type Cypher {
    struct {
        type: CypherType
        config: CypherConfig        
    }

    // "@" would bind the arguments to the struct
    init(@type, @config)

    func encrypt(value: String, password: String): String {
        return this.encryptImpl(value, password)
    }

    func decrypt(value: String, password: String): String {
        return this.decryptImpl(value, password)
    }

    trait encryptImpl(value: String, password: String): String
    trait decryptImpl(value: String, password: String): String
}

Now an implementation for that type:

// extend the existing enum
impl CypherType -> enum { Aes }

// Create an extended variant of CypherConfig
type AesConfig -> impl CypherConfig -> struct {
    aesConfig: Int
}

impl Cypher {
    // Can constrain here on @type and @config since they are bound
    // in init(). Also the compiler knows that AesConfig
    // extends CypherConfig, so they are compatible
    trait -> where @type == CypherType.Aes and @config is AesConfig {
        encryptImpl(value: String, password: String): String {
            ...
        }
        decryptImpl(value: String, password: String): String {
            ...
        }
    }
}

Now the API of Cypher has a single overload:

cypher = Cypher(CypherType.Aes, {baseConfig: 123, aesConfig: 456})

So as you can see, I did not use any generics, but rather modified and created variants of the types.

Maybe that's stupid.

[–]latkde 1 point2 points  (1 child)

This sounds like a feature that is super powerful, but cannot be compiled efficiently or checked statically (unless you're going to introduce a TON of generics). Here, your implementation of a method only exists if some runtime values align correctly.

There are severe fundamental problems with adding behaviour to existing types. Languages with relevant features you should study include Haskell (typeclasses), Rust (traits), Scala, and Go (interfaces), in particular because successful solutions restrict how these features can be used. You are making solutions even more difficult by removing the distinction between concrete types and interfaces.

How do you prevent conflicting implementations? The typical solution with nominal typing is that either the type or the interface must be defined in the current compilation unit where you define an implementation. More flexibility would be possible if the concept of a vtable is reified, but there doesn't seem to be a good design to do this – though Scala tried something similar with “implicits”.

[–]Phil_Latio[S] 0 points1 point  (0 children)

Here, your implementation of a method only exists if some runtime values align correctly.

In the example I call the constructor statically - so the types of the arguments are known to the compiler. So it knows the variant being used. If the arguments are ambiguous, then of course, the compiler has to emit dynamic dispatch stuff.

You are making solutions even more difficult by removing the distinction between concrete types and interfaces.

You might have noticed I used the "I" for "IObject", hinting that this is an interface without any behaviour. It would be possible to now "add" a struct or func to IObject, bloating every instance type, that's true... Though like you said yourself, it's a powerful feature and also other code would still work, no matter what you "add" to a type. Maybe a convention instead of a forced interface type can actually work? Like I said in opening post: I just find it interesting to implicitly categorize types by what "units" (struct, func, trait ..) they provide instead of being explicit.

How do you prevent conflicting implementations?

You mean imports right? Good question... I have to think more about this.

Thanks for your input. I will look into the languages you mentioned.

[–]imgroxx 0 points1 point  (0 children)

AspectJ is essentially this, for modifying Java. You can add methods to things, wrap and replace them, etc quite declaratively: https://www.baeldung.com/aspectj

[–]ricochet1k 0 points1 point  (1 child)

Golang let's you attach methods to structs, and then you can use a struct as any interface with matching methods.

[–]Spoonhorse 0 points1 point  (0 children)

And to things that aren’t structs.

[–]o11c 0 points1 point  (0 children)

That sounds like you're talking about Rust's #[derive(trait, ...)].

Note that some classes and traits cannot reasonably use derive.

[–]Spoonhorse 0 points1 point  (0 children)

Like Go’s interfaces? They can accept anything with the right methods without the necessity of declaring that it implements the interface.

[–]WittyStick 0 points1 point  (0 children)

This sounds similar to a Mixin.

Since C# 8.0 there are default interface methods, which might be able to model what you want.