kangaru icon indicating copy to clipboard operation
kangaru copied to clipboard

Path to kangaru 5

Open gracicot opened this issue 3 years ago • 10 comments

Here I simply want to discuss what we want for kangaru 5. There are a few points I want to see in kangaru 5:

  • All services should be possible to define using the service map. Right now, autocall and supplied services can't really be done in a practical manner using the service map definition.
  • The API should prioritize the service map. services defined as a struct should be special cases.
  • C++17
  • The container should be a template. The container "allocator" should be possible to change and allow statically defined finite container to support zero overhead.
  • constexpr containers.
  • Simplification to the injection model, that includes invoke and the construct function.

I also want a more "conceptified" set of checks. That should also simplify error messages, which simply don't work anymore.

Now, there are a few points that raises questions:

  • If we go full service map, do we still need the construct function in services?
  • If we go full service map, should we allow simply classes to be used as service in the container instead of the service definition?

There may be other questions. If you have suggestion or feedback, please write a comment here. I want to gather as much ideas and improvement for the version 5 as possible.

gracicot avatar Mar 10 '21 15:03 gracicot

In version 5, I want to go much further and expose all the guts. Let me explain.

Instead of having one container, I want to expose injection building blocks. Then, implement the container above those building blocks.

This will enable much more than what kangaru is doing right now. For example, being constexpr compatible, having a zero overhead interface, support for injection contexts, composing containers together and much more.

I will likely require C++17 and more recent compiler.

gracicot avatar Jan 25 '22 21:01 gracicot

I haven't gave up! I'm just quite busy there days. I plan to create the branch for version 5 soon :)

gracicot avatar Dec 19 '22 11:12 gracicot

if you can squeeze this in, an expected (C++23) for Supplied Services would be nice (so I don't rely on exceptions).

idk if it's possible but maybe you can harden the interface by returning an expected<T, NotFoundError> when the service is_supplied_service or is_abstract_service.

(I'm sure this can mess up the dependencies and it needs to be returned upstream if one dependency is not suppliable)

Pretty sure there are a lot of expected implementations (make sure there are supporting constexpr):

  • https://github.com/martinmoene/expected-lite
  • https://github.com/TartanLlama/expected

abeimler avatar Feb 15 '23 11:02 abeimler

Honestly the exceptions were just a workaround because the way I implemented supplied services requires deferring check to runtime. We'll see how kangaru 5 evolves, but I will most likely add a way to make runtime validation do something like that. My plan is that you could build your own container that acts how you need to using easy to understand building blocks.

Note that technically, you could make a service of excepted<T, NotFoundError> today in kangaru 4 using custom service definition. You can hook into the construction and injection mechanisms. Make it not supplied and make it hold an kgr::optional of your service. When that service is constructed by the container, leave it empty and make forward return a NotFoundError. You can then replace the sevice using container.replace but construct the replacement with a filled optional. The downside though is that this service is always gonna be injected as a excepted, but native support from kangaru would also do that.

The only problem is that custom service definitions are a super handy feature but also really complicated and hard to use, this is one point I want to make easier for kangaru 5.

gracicot avatar Feb 15 '23 14:02 gracicot

Okay, small update. I implemented a way to add injection metadata to types. For example, we can tag a type so kangaru know it's safe to default construct it.

This is great. Kangaru 5 will mostly work with normal types, then I'll implement service definition on top of it. The big difference is that service definition will be optional, and even though kangaru 5 will have containers, users will be able to implement their own containers or injection schemes.

Right now to express the metadata you can either declare friend function in the scope of a type or specialize a type trait.

gracicot avatar Jul 07 '23 23:07 gracicot

How will kangaru 5 handle service instantiation? Currently, when emplace or service is called, the service instance is created immediately. While kgr::lazy exists, it unfortunately lacks support for custom parameters and you need to specify it manually. Another annoying case is that if you have abstract service dependencies, the order in which you add them matters.

For the first case, it would be nice to have a feature similar to .NET Core, where a function could be passed to service or emplace to construct the service with captured variables.

WopsS avatar Feb 11 '24 21:02 WopsS

@WopsS In kangaru 5 there will not be "one" way to instantiate your services. You'll simply build the container that suits your needs. In many of my project, I simply don't need polymorphic services, so I'm gonna compose the container building blocks together in a way that don't support polymorphic. If you need services to be provided by a specific function, or a specific class then the hooks are gonna be there to make a container that reach for those.

Another annoying case is that if you have abstract service dependencies, the order in which you add them matters.

I honestly have no idea how to solve that, and I never used a framework where this wasn't a problem. It was either the first one wins, or the last one wins in the case where many classes implemented the same interface. Other framework that "didn't" have the problem simply threw an exception when your configuration could be conflicting.

Maybe you have a better idea than me how to solve this, as I don't use interfaces and inheritance or polymorphism at all in most of my code.

gracicot avatar Feb 11 '24 21:02 gracicot

It sounds like a good idea. Do you have any examples related to Kangaru 5 for that?

I honestly have no idea how to solve that, and I never used a framework where this wasn't a problem. It was either the first one wins, or the last one wins in the case where many classes implemented the same interface. Other framework that "didn't" have the problem simply threw an exception when your configuration could be conflicting.

Sorry, I didn't provide enough information. The scenario you described is fine with me; I don't have any concerns about services implementing the same interface and replacing each other.

However, what I meant to address is the following situation:

  • Suppose I have two abstract services: IFirstInterface and ISecondInterface.
  • I'm creating two services: ServiceA, which inherits from IFirstInterface, and ServiceB, which inherits from ISecondInterface. ServiceB has a constructor dependency on IFirstInterface.
  • If I add ServiceA using emplace first, followed by ServiceB, everything works fine. But if I add ServiceB first and then ServiceA, an exception occurs because ServiceB can't resolve its dependencies.

An example describing my issue:

struct IFirstInterface
{
    IFirstInterface() = default;
    virtual ~IFirstInterface() = default;
};

struct FirstInterfaceService
    : kgr::abstract_service<IFirstInterface>
{
};

struct ISecondInterface
{
    ISecondInterface() = default;
    virtual ~ISecondInterface() = default;
};

struct SecondInterfaceService
    : kgr::abstract_service<ISecondInterface>
{
};

struct ServiceA : public IFirstInterface
{
    ServiceA() = default;
    ~ServiceA() = default;
};

struct ServiceAService
    : kgr::single_service<ServiceA>
    , kgr::overrides<FirstInterfaceService>
{
};

struct ServiceB : public ISecondInterface
{
    ServiceB(const IFirstInterface& service)
        : m_service(service)
    {

    }

    ~ServiceB() = default;

private:
    const IFirstInterface& m_service;
};

struct ServiceBService
    : kgr::single_service<ServiceB, kgr::dependency<FirstInterfaceService>>
    , kgr::overrides<SecondInterfaceService>
{
};

int main()
{
    {
        // This works, me happy.
        kgr::container m_services;
        m_services.emplace<ServiceAService>();
        m_services.emplace<ServiceBService>();
    }

    {
        // This doesn't work, exception thrown :(
        kgr::container m_services;
        m_services.emplace<ServiceBService>();
        m_services.emplace<ServiceAService>();
    }

    return 0;
}

WopsS avatar Feb 11 '24 22:02 WopsS

@WopsS I see. I had another user that wanted abstract services but to be able to provide a callback that created the issuance for it. That would fix this problem. It's not impossible in kangaru 4 today, just quite convoluted to achieve. I think I'll open an issue for this one, it wouldn't be that hard to add in.

gracicot avatar Feb 12 '24 13:02 gracicot

In addition to the specific you are mentioning, doing lazy initialization of the service by default (or a having a building block doing this) would be a nice addition, which would solve the example in https://github.com/gracicot/kangaru/issues/101#issuecomment-1937893897.

WopsS avatar Feb 12 '24 20:02 WopsS