Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Functional Programming #42

Open
alnewkirk opened this issue Jan 28, 2015 · 7 comments
Open

Support Functional Programming #42

alnewkirk opened this issue Jan 28, 2015 · 7 comments
Labels

Comments

@alnewkirk
Copy link
Contributor

The only thing required to support dependency injection with function programming is a feature-flag in the configuration file which tells the container not to treat the first argument as an invocant.

@preaction
Copy link
Owner

Are you talking something like the MooX::NonOO module, where you can use Beam::Wire methods without needing an object, getting a magic singleton?

Or when trying to satisfy a dependency and right now it always requires that it be a class with a constructor that takes the given args?

Or is this more related to #11? The problem with doing subs as dependencies is that they shouldn't be serialized into the config file, so I was thinking if we could do like Bread::Board does and have a DSL for constructing a container, then the services could be any valid executable Perl code.

Or are you using Config::Any's Perl-based config file support to do what #11 wants (does that work?) and then need to ensure that the container knows you've got a different kind of dependency?

Do you have an example of how the result would look when being used? We haven't yet had to add a container-level config value to the configuration files, but that doesn't mean we'll always be that lucky. And if it works nice, we can figure out an implementation.

@alnewkirk
Copy link
Contributor Author

Sorry for the confusion; None of the above describes what I'm talking about. Currently, Beam::Wire is all OO, however, there are many people moving towards the functional programming paradigm. Functional programming, although not OO, still has the need to dependency injection/management, composition, etc. The gist of what I'm saying is that Beam::Wire could also support this style of development (I've been moving toward functional myself). Essentially, Beam::Wire would generate functions and support high-order functions and return them. Also, we'd be able to say that Beam::Wire is now the only DI container supporting functional programming in Perl.

Here's how is might work:

rabbitmq:
    class: RabbitMQ
    args:
        host: example.com
        port: 61312
        user: root
        password: secret
enqueue:
    class: MyApp::Functions
    function: rabbitmq_enqueue
    args:
        - { $ref: "rabbitmq" }

... so what does this do exactly? Well, it defines an object and a function, the function takes the object as a parameter. The enqueue function returns a coderef which when executed calls the MyApp::Functions::rabbitmq_enqueue function with the RabbitMQ object as it's first argument. The implementation might look something like:

sub enqueue { local @_ = ($rabbitmq_object); goto &MyApp::Functions::rabbitmq_enqueue; }

The usage might look something like:

my $enqueue = $wire->get( 'enqueue' ); $enqueue->();

Which is super cool in my opinion? Also, we'd be able to support high-order functions by allowing functions to depend on other functions essentially creating coderefs which take coderefs as arguments. I hope you're in agreement, I think this addition will make Beam::Wire extremely powerful and stand out above anything else.

What do you think?

@preaction
Copy link
Owner

Yes. I like it. By declaring a service (we know we're declaring a service because we're in the top level) with "function" (could it be "sub" instead?), we are going to return a subref. The args would let us curry the returned subref (so one could do $enqueue->( $msg ) and the underlying sub would get the $rabbitmq_object, $msg as args).

And yes, you're right. Functional programming has the same need for dependency injection. Maybe even a bigger need: every Moo attribute is a dependency that can be injected, but how do you wire up subs? Well, how about Beam::Wire!

Do we use sub anywhere yet? I'm fairly sure we use method. It'd be interesting if, as you show, object methods could be magically wrapped in subs (so if instead of MyApp::Functions::rabbitmq_enqueue it was the actual method RabbitMQ::enqueue that requires a RabbitMQ object as its first arg), exposing itself as a functional API on what is otherwise an object. If people have to learn a bit about how Perl objects work (the magic first arg) that isn't the end of the world, but it'd be interesting if they didn't, and just said "expose this method as a subref".

But I can also see the idea that a service is the value returned from the sub call and not the sub itself. So maybe that's a distinction between "function" in the service description and "sub" or "method" (though I like making people conform to best practices by making better practices easier, and this new thing is a lot more awesome, so it should get priority for a shorter keyword). Or maybe "call the sub" is "call" instead. Whatever, not important right now.

So yes, we should definitely try this. And don't let my above bikeshedding slow you down.

@preaction
Copy link
Owner

So, combining this with #43, #46, and #47, I've come up with the following idea: There should be a small number of meta-keys:

  • Service creation keys:
    • $class - Always a class name
    • $extends - A ref name to merge config from
    • $constructor - The name of the constructor, defaults to "new"
    • $args - Arguments to the constructor
    • $with - An array of roles to compose, potentially supporting parameterized roles in the future
    • $lifecycle - Same as before
    • $on - Bind to events. This requires subrefs, using the $sub dependency below (though, if a method call returns a subref somehow, $call could work, and if a service was already a subref because of a $sub, $ref could work)
    • Any service that is not a hash of strictly meta-keys is not an object, it's a value (though it can contain dependencies inside)
  • Dependency injection keys
    • $ref - If a string, the service name to refer to. If a hashref, create an object with the above object creation keys.
    • $value - This can be a string of Data::DPath (like presently), or a hashref with other ways of selecting things from a data structure (only dpath will be supported at the start).
    • $call - A method name to call to get a value. This can be a simple string, or a hashref with $method, and $args, and potentially $value to use Data::DPath to traverse the value returned by the method.
    • $sub - The same as $call, except returns a subref that will do the thing when called. If a string, the method to bind to. Arguments to the subref returned will be passed to the method. If a hashref, $method is the method to bind to, $args are arguments to put at the start of the method call (currying), and $value for post-processing the value returned.
    • $method - Deprecated for being too confusing, since it was used to do more than one thing: In some contexts it would call the method. In others, it would return a sub that would call the method. In service creation, it was the constructor. Replaced with $call and $sub

Fuck. I've created a programming-by-config language. Next obvious step is making a real language using Marpa... Well, a bunch of this stuff isn't going to be used all the time. The core bit is "service creation" and $ref. I'm also not going to implement all of this, especially the things that I don't see being used often, I just note them for future possibilities to see how the config syntax will allow for growth. I think having meta-keys allow for hashrefs inside will help with future-proofing.

I'm going to write the docs for this new stuff first, in a branch, and get some feedback on it before I release v2.0 with this new stuff. I think I can get mostly backwards-compatible, considering the new functionality is mostly new config that is currently invalid.

@alnewkirk
Copy link
Contributor Author

This specification/proposal is awesome. I love the new approach and functionality. I think we should be mindful in the terms we use so we don't confuse people who might want to use this system. For example, the $sub versus $call terminology. Also, I honestly think we should have dropped $sub (which we have), and I think we should use $method and $function independently, because those distinct terms describe how the routine should be invoked.

The term $sub is too ambiguous, because it could be describing a routine that takes a class instance as its first argument, or not. Using $method is somewhat explicit as it implies that the routine should be invoked as method on a class, and $function is also explicit as it implies that the routine should not be invoked as method on a class.

Additionally, we could use an "engine" abstraction (factory pattern) to support backwards compatibility, and as we evolve the configuration language, I see the need for backwards compatibility even more. I'll keep my eye out for the v2.0 branch so that I can help implement it. I use BW daily, I love it, and I'm looking forward to the v2.

@preaction
Copy link
Owner

$sub was added to the event handler stuff with the last release. The idea is that is what you're getting: a sub. We should be careful not to use synonyms, which is what I hope to do by dropping $method in favor of $constructor (or, perhaps liberating $method from its current position as 3 different things).

The larger idea is to stop trying to use combinations of meta keys as indications of what should be done, trying to DWIM breaks down when I could mean two different things. Meta keys with their own internal keys is far better.

I was giving some thought to non-object methods (plain subs, functions) as services, somehow declaring a service from nothing without an object to instantiate, and it brings in a bunch of complications, not the least of which is crossed terminologies. But I think we should do it, so I imagine we'll do some experimenting in a branch.

Oh, and I just thought of a few cases where it would be massively useful for me (File::ShareDir::dist_dir is one big one, since so many of my configuration files are in share dirs), so here's an off-the-cuff idea: Perhaps using $package instead of $class indicates that we are not creating an object and then $sub becomes sub { return $package::$sub( @_ ) } (returning a subref), $call becomes $package::$sub() (calling like a sub and getting the value returned), $value is a package variable, and $method becomes $package->$method() (calling like a class method, no real change from $class and we should probably discourage this but allow it because why not). There'd be no defaults for any of those, so declaring $package requires declaring one and only one of $sub, $call, $value, or $method.

My idea for if it turns out we need backcompat is a special, top-level $config or $version key in the hash. This could also be how one can assign event handlers to the container as it parses its config. As long as the file remains readable as format it's in and not some craziness with comments or headers or something. I hope more that we can find clever ways around this, without compromising the ease-of-use.
I pushed a way to deprecate things, or warn about ambiguous constructs in 9e49fc7, which I hope we can use instead of fracturing the config into versions, but we'll do it if we need to...

One of the things I've been trying to design for is this: https://github.com/preaction/Beam-Runner. The idea is that a Beam::Wire container is basically a string (YAML, JSON, or otherwise) that describes how to create objects, and we can ship that string as a message to another machine, have it create the object, run whatever method we want, and give us the result back, possibly running multiple things, and possibly farming out all the things to a huge cluster. And, looking over that again, $method is a short, obvious word and we should keep using it (though maybe not to refer to everything that Perl refers to as a method, because of the ambiguity).

@iamalnewkirk
Copy link

So, combining this with #43, #46, and #47, I've come up with the following idea: There should be a small number of meta-keys:

  • Service creation keys:

    • $class - Always a class name
    • $extends - A ref name to merge config from
    • $constructor - The name of the constructor, defaults to "new"
    • $args - Arguments to the constructor
    • $with - An array of roles to compose, potentially supporting parameterized roles in the future
    • $lifecycle - Same as before
    • $on - Bind to events. This requires subrefs, using the $sub dependency below (though, if a method call returns a subref somehow, $call could work, and if a service was already a subref because of a $sub, $ref could work)
    • Any service that is not a hash of strictly meta-keys is not an object, it's a value (though it can contain dependencies inside)
  • Dependency injection keys

    • $ref - If a string, the service name to refer to. If a hashref, create an object with the above object creation keys.
    • $value - This can be a string of Data::DPath (like presently), or a hashref with other ways of selecting things from a data structure (only dpath will be supported at the start).
    • $call - A method name to call to get a value. This can be a simple string, or a hashref with $method, and $args, and potentially $value to use Data::DPath to traverse the value returned by the method.
    • $sub - The same as $call, except returns a subref that will do the thing when called. If a string, the method to bind to. Arguments to the subref returned will be passed to the method. If a hashref, $method is the method to bind to, $args are arguments to put at the start of the method call (currying), and $value for post-processing the value returned.
    • $method - Deprecated for being too confusing, since it was used to do more than one thing: In some contexts it would call the method. In others, it would return a sub that would call the method. In service creation, it was the constructor. Replaced with $call and $sub

Fuck. I've created a programming-by-config language. Next obvious step is making a real language using Marpa... Well, a bunch of this stuff isn't going to be used all the time. The core bit is "service creation" and $ref. I'm also not going to implement all of this, especially the things that I don't see being used often, I just note them for future possibilities to see how the config syntax will allow for growth. I think having meta-keys allow for hashrefs inside will help with future-proofing.

I'm going to write the docs for this new stuff first, in a branch, and get some feedback on it before I release v2.0 with this new stuff. I think I can get mostly backward-compatible, considering the new functionality is mostly new config that is currently invalid.

@preaction I wanted to give you a heads-up that I just released Rewire, a reimagining of this project with support for all of the construction strategies but with some important differences which makes the codebase and configuration simpler, also this:

  • Concept of an engine
  • Flexible chaining via build steps
  • Engine ruleset schema with validation

See https://github.com/iamalnewkirk/rewire. Let me know what you think!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants