Giter VIP home page Giter VIP logo

utils.modular's Introduction

utils.modular

Utilities for creating modular systems: functions related to Component, or protocols.

Installation

[com.nedap.staffing-solutions/utils.modular "2.2.1"]

Synopsis

nedap.utils.modular.api/implement

implement is a safer layer over raw metadata-based protocol extension.

Metadata-based protocol extension has recently proven to be a reliable solution against this problem.

In plain Clojure you might be tempted to do:

(defn start [this] ...)

(defn stop [this] ...)

(def my-component
  "A com.stuartsierra/component implementing some functionality"
  ^{`component/start start
    `component/stop stop}
  {})

However, several things might go wrong:

  • What if the protocol lacks a :extend-via-metadata directive?
  • What if you're running Clojure < 1.10?
  • What if the component/stop quoted symbol does not get expanded to its fully-qualified name?
    • Would happen if you forget the :require in your (ns ...) declaration
  • What if component/start does not resolve to a protocol function?
    • e.g. to a function of not emitted by defprotocol
  • What if start does not evaluate to a function?
    • i.e. any other kind of value

implement guards you against all of those, preventing the lack of errors (or opaque errors, at best) that you'd get otherwise.

It also provides some sugar (symbols don't have to be quoted) and enforcement (you cannot pass anything other than a symbol; this aims to avoid deeply nested, non-reusable code).

This is how it looks like:

(implement {}
  component/start start
  component/stop  stop)

nedap.utils.modular.api/add-method

clojure.core/defmethod does the following:

Creates and installs a new method of multimethod associated with dispatch-value.

add-method does the same exact thing, but skipping the Creates part. i.e., it merely associates an existing function to a multimethod.

This has multiple advantages:

  • One can code with plain defns, making things more homogeneous
    • And decoupled, reusable
  • Said defns can be speced.def ones
  • Importantly, one should understand that defmethod is a side-effect, and as such should be controlled.
    • Better to add-method in a Component start definition.

nedap.utils.modular.api/dependent

Helper fn for com.stuartsierra.component/using which takes a dependency collection and optionally a map with renames. Note that the dependencies can be passed as a vector or a map.

(dependent (my-component/new)
           :on my-component/dependencies
           :renames {:internal ::my-component/external})

This allows the user to keep using the my-component/dependencies-def while maintaining the flexibility to rename some keys.

nedap.utils.modular.api/omit-this [f]

Creates a replacement for f which drops the first argument, presumed to be of "this" type. Apt for protocol extensions, when f is an arbitrary function which may not participate in our protocols at all.

Refer to its test for an example.

ClojureScript compatibility

All the offered API is compatible with vanilla ClojureScript (i.e. non-self-compiled).

However, implement offers weaker guarantees in its cljs version, since cljs has fewer introspection capabilities, particularly at macroexpansion time.

At the same time, as long as your cljs code is defined as a .cljc file and it is compiled for the two possible targets (JVM, js), then the JVM target will provide the guarantees that cljs cannot provide. i.e. cross-compilation can act as a "linter", even if only using in production just a single target.

ns organisation

There is exactly 1 namespace meant for public consumption:

  • nedap.utils.modular.api

By convention, api namespaces are deliberately thin so you can browse them comfortably.

Documentation

Please browse the public namespaces, which are documented, speced and tested.

License

Copyright © Nedap

This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0.

utils.modular's People

Contributors

tcoenraad avatar thumbnail avatar vemv avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

utils.modular's Issues

Dynamic protocols

I think a variant of defprotocol that replaced the 'receiver' argument (this) with an auto-generated dynamic variable would yield much more pleasant codebases to deal with, reducing the friction of using protocols and Component.

(dynamic/defprotocol Foo
  (do-it [x y]) ;; will emit something like `(--do-it [this x y])`
  ;; will also emit *this* var, shared across all protocol methods
  ;; this assumes only one protocol exists per ns, which is the recommended pattern anyway (soon to be enforced)
  )

(defn do-it-impl [x y] ;; a protocol method implementation. No receiver argument necessary 
  (println (+ x y))
  (println *this*))

(implement {}
  --do-it do-it-impl)

;; In consumer code: ----------

;; this is where the proposed mechanism shines.
;; I don't have to find (and pass around) a component implementing a protocol:
;; Component will bind *this* to the adequate component.
(do-it x y)
;; So, programmers can have the illusion of using plain functions instead of components or protocols at all,
;; reducing the perceived indirection of having clean/testable code.

dynamic/defprotocol should be built on top of speced/defprotocol.

Just an idea for now, might be unfeasible

Local environment is disregarded

The following code will succeed:

(def f inc)

(let [f 2]
  (implement {} some/protocol f))

...but that's counterintuitive. Under the covers, the f local shadows #'f, but the latter is the one that actually gets used.

Ideally, an exception would be thrown if f shadowed an #'f, because it is not intended usage to pass let bindings to implement.

dependency renaming

Rational

Renaming dependencies of components is sometimes necessary. Currently in context of the dependencies-fn that most components have it's an "all or nothing" situation.

e.g. either your system maps looks like this:

::taxes/component (-> (taxes.component/new)
                      (component/using common-dependencies))

or like this:

::taxes/component (-> (taxes.component/new)
                      (component/using 
                        {::environment/component ::other-environment
                         ::config/component      ::config/component }}))

Notice; we only want to change the key of the ::environment/component-dependency; but we have to list (and therefore require the kws of) all dependencies.

Proposal

Simply transforming the component/dependencies-vector to a map and renaming some keys would be enough

(defn using-renamed [component dependencies & kvs]
  (component/using component
   (apply assoc (zipmap dependencies dependencies) kvs)))

This allows the following;

::taxes/component (-> (taxes.component/new)
                      (using-renamed taxes/dependencies
                       ::environment/component ::other-environment/component }}))

resulting in less fuss, more focus on different things, and allowing the taxes/dependencies to grow without getting out of sync / being duplicated.


This issue is different from #10 which focuses on the semantics regarding the config; although this issue might help remove duplication in whatever solution we choose there :)

`implement`: Forbid nil args?

Problem statement

(implement nil ...) probably is a bug 100% of the times. However the library doesn't fail-fast on it.

See e.g. nedap/components.pedestal#7

Proposal

Validate presence of implement's first arg.

Alternatives and comparison

Do nothing? It might make sense (given nil is a valid protocol extension point), seems unlikely though (you cannot attach metadata to nil).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.