[moving to scala-debate per agreed request]
I don't recall your constraints so cannot answer your question.
Someone (I forget who) recently wrote (I forget where): "DI is just
socially acceptable global variables." This is mostly true -- I say
mostly because I think the adverb "globally" is redundant and
misleading. That is to say, there is no such thing as a global variable.
All variables are scoped to some context and it is the extent of this
context that is a measure of detriment. This is why you hear people
talking about "keeping their side-effects local." This wishful-thinking
almost never eventuates because side-effects are pervasive. I am
side-tracking here, but going back to the original topic briefly.
In the absence of my awareness of your constraints, I can point out what
it is that most people want when they think they want DI, and in fact,
do not, ever (it is one of many forms of masochism in programming --
bare with me).
First, let us consider a general Scala program and generalise it. This
is just an arbitrary program -- I am trying to make it as convoluted as
possible so that you can go back to a real program and apply the same
reasoning. Importantly, this program is side-effect free at this point.
val a = e1
val b = e2(a)
val c = e3(a, b)
val d = e2(b)
OK, now I am going to generalise it by running the same program, but in
a for-comprehension. We do this by following these rules:
1) Remove the 'val' keyword
2) The = symbol becomes <-
3) We wrap the program in for and yield
I am going to create a data type that simply wraps a value and provides
flatMap and map methods so I can do this:
case class Id[A](i: A) {
def map[B](f: A => B) = Id(f(i))
def flatMap[B](f: A => Id[B]) = f(i)
}
...and since I don't want to explicitly wrap/unwrap my values with Id, I
am going to provide an implicit for in and out:
object Id {
implicit def IdIn[A](a: A) = Id(a)
implicit def IdOut[A](a: Id[A]) = a.i
}
OK, so now let's translate our program:
for {
a <- e1
b <- e2(a)
c <- e3(a, b)
d <- e2(b)
} yield d
Now that you accept that any program can be written this way, let us
step away for a moment and address the idea of DI. There are usually two
variations on DI:
1) The "configuration" (or context) is done and the application must
start by first initialising this context, then the application may run.
The application then reads from the configuration during run-time but
does not modify it. If this order is altered, you end up with a broken
program. A "DI" container attempts to promise you that no such thing
will occur -- this is essentially what the selling point is.
This dependency on explicit execution order is directly anti-thetical to
the functional programming thesis. This is a consequence of there being
a widely-scoped variable that kind-of pretends otherwise.
If you turn your head just a little, you can see this is a somewhat
degenerate notion of what is called "uniqueness typing." I digress.
2) Same as above, however, not only is the application permitted to read
the configuration, but it is also permitted to *write* to it. This means
that the application depends on *more* explicit execution order and the
possibility of bugs increases even more.
Imagine if I said, "you know what, turn all that DI stuff off, we are
going to initialise our values up front and pass them all the way
through the application." You would surely protest, "but that is so
clumsy!" and you'd be right, but only at first glance.
You see, there is a way to pass these values through quite neatly and
no, this is not using Scala's implicit keyword (which is insufficient),
this is something else. OK, so let's first start by thinking about case
1) above where the application only has read access to some context. I
will name this context, "Context", it is a data type that is
somewhere-or-other that we would like to pass through our application --
but no writes to it. I'm sure you can imagine what Context would really
be -- feel free to make it up for the use-case.
So, our values that were once mere values, are now computed as if they
have access to a Context. We can denote this with a data type:
case class ComputedWithContext[A](cx: Context => A)
So we now have "first-class" values computed with a Context, rather than
being mere values. We can now create these by accessing a Context "as if
it were passed" -- that is to say, although we don't yet have a Context,
we may create values that access that Context (when it is eventually
passed) by wrapping a function (or a trait if you prefer).
This is simple and straight-forward enough. But watch this:
case class ComputedWithContext[A](cx: Context => A) {
def map[B](f: A => B): ComputedWithContext[B] = ComputedWithContext(f
compose cx)
def flatMap[B](f: A => ComputedWithContext[B]): ComputedWithContext[B]
= ComputedWithContext(c => f(cx(c)) cx c)
}
We see here that ComputedWithContext happens to have pretty handy map
and flatMap methods. What can we do with them?
OK, so suppose our program above is a little different to the original
in that actually, our expressions (e1, e2 and e3) require a Context, so
each of them becomes become ComputedWithContext[T] where previously they
were just the type T (they may be all different values for T or same --
no matter).
For example, e1 may have been an Int where now it is a
ComputedWithContext[Int] and e2 may have been a String where now it is a
ComputedWithContext[String]. You get the point.
Here is how our program looks:
for {
a <- e1
b <- e2(a)
c <- e3(a, b)
d <- e2(b)
} yield d
This is precisely the same program syntax. The type of this expression
is ComputedWithContext[T] where the type T depends on the value d. In
other words, we may pass a Context in to this value and it gets
"threaded" through our program and our program *doesn't change* if we
write it in this general form. We may "stack these layers" on top of
what started as Id and our program remains unaltered. The "theory" of
doing this is quite involved, mostly because it is kick-arse interesting
and we could talk about it some time, but that's another story!
Importantly, there are no variables here. Not one and not a pretend
value that is actually a variable at application time (which I'm sure
you've been reminded of more than once when using DI).
So, this is how we deal with passing read-only context through our
application:
* without being clumsy by explicitly passing it
* being quite efficient and readable in fact!
* without using variables that leads to program bugs and difficulty
reading and debugging code
How do we deal with read and write values (case 2)? Well, we need a new
different data type for that:
case class WriteWithContext[A](cx: Context => (A, Context))
Notice how this is the same data type as before except the function can
now produce a *new* Context as well as the computed value (paired). This
is to say, we may "modify" the Context as it is threaded through. But
what about map and flatMap, can we write those? Of course:
case class WriteWithContext[A](cx: Context => (A, Context)) {
def map[B](f: A => B): WriteWithContext[B] = WriteWithContext(c => val
(a, cc) = cx(c); (f(a), cc))
def flatMap[B](f: A => WriteWithContext[B]): WriteWithContext[B] =
WriteWithContext(c => { val (a, cc) = cx(c); f(a) cx cc })
}
Don't get too carried away with reading those methods, but just note
that flatMap "threads the Context through whatever the function is,
which may be modifying it."
OK, so now if we suppose that our expressions (e1, e2, e3) actually had
access to the Context, but were also able to "modify" it by returning a
new Context (or just leaving it alone, for which there is library
support of course), then our program would look like this:
for {
a <- e1
b <- e2(a)
c <- e3(a, b)
d <- e2(b)
} yield d
Yep, exactly the same as before. So now we have a value that we can pass
in a Context and it is threaded through the program, potentially
"modifying" the Context as it is threaded through and we get a value and
the resulting Context at the end. We may wish to drop either of these --
in practice, the Context often gets dropped, since it was only need to
compute the value -- and of course, there is library support for that.
So hopefully now you see that DI can be replaced by a superior
programming model, at least for this example, and I promise, for any
example. We just have to come to terms with a few data types and
abstractions and we can kick that baby to the gutter where it belongs.
Hope that helps!
- --
Tony Morris
http://tmorris.net/
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/
iQEcBAEBAgAGBQJObfLOAAoJEPxHMY3rBz0Ps/oH/iXV+fEk9hGdUN0TbdcqR3Fh
QlwVc3VN9El4jLb2yQdopL+93Tov3EYn1beVcC1R7ptI75jtrmRcppPMaJdRUFnN
jeDvqXghac0+evt8zoaK2GqGq1H3R8eG6kdx5pBjf+0PCiJS9RziRQpITb+5Kob2
0I6MSe+beNe7UcVX7HGp9oGVx56CTaieh2R+H6LtGlhwahh//6BBeEyLflIGdc4w
8O8oJwfxT5lnsn2aXsxveB+zkywNyl+dxPEk83o5E3AVIKCvaEnRTKmwd4LsHIQM
D/KVVyaPSKyvziEaVNwAlsukC3LoYg+MBhq9jRAJ65wCEhB+M8oUHNjgnDDbloA=
=J7D1
-----END PGP SIGNATURE-----
This approach seems very nice. What if the ComputedWithContext type is externalized and the classes that implement it are dynamically loaded based upon the environment it runs in (i.e. the domain)?
If this is how the book is going to be, I can't wait!
Cheers,
Razie
-----Original Message-----
From: scala-...@googlegroups.com [mailto:scala-...@googlegroups.com]
On Behalf Of Tony Morris
Sent: September-12-11 7:54 AM
To: scala-...@googlegroups.com
Subject: Re: [scala-debate] Dependency Injection
I think this is a fantastic email; however there is one caveat - you imply that the "program" is unchanged, which is true for the semantic structure you have defined.It is not true for the program constructs themselves, though (i.e. e1, e2 and e3), where the ComputedWithContext permeates both their input and output.What is more, a program which uses many constructs e1, e2 ... eN, all of which are ComputedWithContext, for which, upon fulfilling some new requirement, *a single one* must be changed write to its context too, you must change the input and output arguments to all e1 ... eN.
Perhaps with Haskell this is trivial because of the level of inference going on, but it is a concern in scala, no, that a tiny change might result in hours of development overhead? Are these reasonable observations?
I don't recall your constraints so cannot answer your question.
scala> implicit def WriteWithContext_is_ComputedWithContext[A](w : WriteWithContext[A]) : ComputedWithContext[A] = ComputedWithContext[A](cx => w.cx(cx)._1)WriteWithContext_is_ComputedWithContext: [A](w: WriteWithContext[A])ComputedWithContext[A]
scala> implicit def ComputedWithContext_is_WriteWithContext[A](c: ComputedWithContext[A]): WriteWithContext[A] = WriteWithContext[A](cx => c.cx(cx) -> cx)ComputedWithContext_is_WriteWithContext: [A](c: ComputedWithContext[A])WriteWithContext[A]
Hi Russ,
You can also use something along the lines of my ssconf [1] to compile
a config file at runtime.
trait MyConfig extends SSConfig {
val alphaTrackFilter = new Value(0.75);
...
}
And your config file looks like this (it will be compiled at runtime):
alphaTrackFilter := 0.80
best, Erik
Billy
> Sent from my free software system <http://fsf.org/>.
Hi Russ,
You need to bundle the scala-compiler jar with your runtime, but it’s
the same thing the REPL does, basically.
Here is what a syntax error looks like (it throws a
java.lang.Exception, so you can catch errors when loading your
configs):
/tmp/configurator3129450973169847382scala:4: error: not found: value XXX
traitTest := XXX;
I suppose it would be an internal DSL, because the syntax is
(almost-)pure scala, (I say almost because we append some wrappers
around the text so that config files can look like:
foo := 1;
bar := true;
without the boilerplate object definition wrapped around it.
best, Erik
On 09/12/2011 09:38 PM, Billy wrote:
You're very close.
There are three "traditional" types of data structure that capture these
ideas:
1) Reader -- reads an environment to compute but does not modify it.
2) Writer -- writes to an environment without any inspection/read of
that environment (think: logging)
3) State -- reads and environment and potentially modifies it.
There is also a fourth that combines all three called RWS or
ReaderWriterState in Scalaz7. This is probably a closer description of
dependency injection itself, but the explanation would have got a bit
confusing. Further, all four of these can (and often are) be "lifted" to
thread yet another environment through.
The two cases I gave were Reader and State.
Scalaz hasn't implemented MonadWriter and MonadReader type-classes for
no other reason than nobody has got to it yet! Going to be that one? :)
This sounds like case 1) aka "Reader." Happy to help if you can give
specifics, but it's really not much more than building a library around
a function as given.
>
> My current approach eliminates the need to recompile. Instead of using
> an object, I use a class called Config that has defaults for all
> switches and parameters and a constructor that accepts command-line
> arguments to override those defaults if desired. My main program
> passes the command-line arguments, if any are provided, to an
> instance of the Config class, and I use my class called "CommandLine"
> to parse those arguments. This allows the user to set a few important
> switches and parameters without recompiling (most of the parameters
> are rarely varied, so they don't need to be configurable from the
> command line).
The whole "need to recompile" problem can get tricky and is also quite
contentious (makes a great fire-starter). I have my own opinions on
this, which are also shared by a few others, but all I can say for now
is: try to avoid it if you can, and if you cannot, push it way way to
the outside. Hope that helps. Sorry for the dodge.
I don't recall your constraints so cannot answer your question.
The devil's in the details. For those not familiar with the constraints around a solution for my argument, here they are as posted previously:
{quote}Perhaps a use case for DI might be in order. Scenario: developing a (very) large solution in field F for a specific domain D that provides service type T. A new domain D2 is later acquired that falls in F and must provide T. Every aspect about T is the same except for the underlying business rules that apply to the data for D2. Without DI, one would be left maintaining an entirely new branch of T as a separate solution rather than simply injecting those classes which apply the rules for the domain. Now, compound this with new domains being added every year or two (sometimes multiple at one time).
{/quote}
Now, whenever a change to the codebase occurs (within the enterprise), it must be redeployed to all domains to maintain a level environment due to support and maintenance. Thus, recompiling the entire solution for a few altered rules, regardless where they reside in the targeted source, results in all domains needing redeployed. Said deployment can consume up to 20+ man hours _per domain_. DI solves this because the rules for each domain Dx are the only portion that gets compiled and added to any new domains.
From a purist FP stance, DI may be anti-thetical. However, scala is not a pure FP platform and pure FP cannot solve this matter (of recompilation and redeployment) to the best of my knowledge.
Thus, I turn to scala's object-functional nature to leverage it as best as possible given the constraints that it may not be a pure FP solution. The results, I hope, will be the best DI container for the foreseeable future that allows for functional solutions to problems in a way that has never before been seen.
Billy
1. be able to flip switches and change parameter values (between runs)
without recompiling
2. keep my configured classes independent of any global configuration
object (excluding their own companion object) so they can be re-used
in another application without modification or recompilation
3. avoid variables
By using my own straightforward methods, I seem to be able to achieve
any two of these three objectives but not all three.
On Fri, Sep 16, 2011 at 12:46 AM, John Nilsson <jo...@milsson.nu> wrote:
On Fri, Sep 16, 2011 at 9:07 AM, Russ P. <russ.p...@gmail.com> wrote:
1. be able to flip switches and change parameter values (between runs)
without recompiling
2. keep my configured classes independent of any global configuration
object (excluding their own companion object) so they can be re-used
in another application without modification or recompilation
3. avoid variables
On Fri, Sep 16, 2011 at 12:46 AM, John Nilsson <jo...@milsson.nu> wrote:
On Fri, Sep 16, 2011 at 9:07 AM, Russ P. <russ.p...@gmail.com> wrote:
1. be able to flip switches and change parameter values (between runs)
without recompiling
2. keep my configured classes independent of any global configuration
object (excluding their own companion object) so they can be re-used
in another application without modification or recompilation
3. avoid variables
Have you tried something like that:import java.lang.ThreadLocalclass Parameter[A](initial: A) {val tl = new ThreadLocal[A] {override def initialValue() : A = {initial}}def apply() = tl.get()def withValue[B](a : A, p : => B){val old = tl.get()tl.set(a)try{p}finally{tl.set(old)}}}
With a companion object:
object Parameter {def getConfiguration[A](name : String)(implicit m : Manifest[A]) : Option[A] = None// should read the configuration heredef apply[A](name : String, default : => A) : Parameter[A] =new Parameter(getConfiguration(name).getOrElse(default))def apply[A](name : String): Parameter[A] =apply[A](name, error ("Configuration Error. Not found or not valid: " ++ name))}It allows to separate the concern of reading config file.It also allow change locally a parameter, which can be useful.And it prevents to modify it.Best,Nicolas.
To make sure you have a consistent view of the configuration for the
duration of one process (thread) you need make sure that on a thread, only
one version of that configuration is used. This solution will pass that
specific version to all members.
You could use a ThreadLocal copy, or come up with your own context passed
around, but you're now on a sliperry slope...
What I would really like to see is a monad that can do this across actors,
eh? Assume the steps of processing are undertaken by individual actors not
simple functions that I can chain in a for, but actors chained by
messages... AND, when I see that, I'd like it distributed too :)
Thinking through this problem in depth you'll find that you've just began to
scratch the surface :)
-----Original Message-----
From: scala-...@googlegroups.com [mailto:scala-...@googlegroups.com]
On Behalf Of Russ P.
Sent: September-16-11 3:07 AM
To: scala-debate
Subject: [scala-debate] Re: Dependency Injection
Was I really that unclear?
Here's a practical application of this approach versus the static object:
reloading configuration at run-time.
To make sure you have a consistent view of the configuration for the
duration of one process (thread) you need make sure that on a thread, only
one version of that configuration is used. This solution will pass that
specific version to all members.
You could use a ThreadLocal copy, or come up with your own context passed
around, but you're now on a sliperry slope...
What I would really like to see is a monad that can do this across actors,
eh? Assume the steps of processing are undertaken by individual actors not
simple functions that I can chain in a for, but actors chained by
messages... AND, when I see that, I'd like it distributed too :)
Usually that’s the case, but it’s a little bit simplified for this narrow purpose.
If the version number V of the configuration required for that logical thread accompanies the messages for that work unit, every node could retrieve the proper V for that part of the work unit, especially if they agree to keep the last few versions for a while and then you need this backed up by some common git as a fallback – it may get hairy to implement fully, but not that hard for a 99% success rate.
up a read-only conflguration for an application, where the switchesAs I explained in my earlier reply to this message, I'm trying to set
and parameter values are all constant for each run. I would like to
1. be able to flip switches and change parameter values (between runs)
without recompiling
2. keep my configured classes independent of any global configuration
object (excluding their own companion object) so they can be re-used
in another application without modification or recompilation
3. avoid variables
By using my own straightforward methods, I seem to be able to achieve
any two of these three objectives but not all three. Can Tony's method
achieve all three? If so, then I'd say he has something significant.
If not, then it seems to me that his approach is nothing more than an
alternative (and more complicated) way to achieve something that can
be done fairly easily. I'd like to know which it is.
I don't think I follow completely. You want to reference some configuration, but you don't want a dependency on it? The mere fact that you are referencing it is a dependency no matter what technique you use to resolve it.
In any case, if you want a more "hidden" reference you can maybe create factories that does "new MyConfiguredClass with TheConfiguration" for you.
Or you can move the dependency from the classes as such and move it to the operation on the class. def aConfigurableOperation(i:Input, cfg: Config)
or even
def aConfigurableOperation(i:Input)(implicit cfg:Config)
or a variation on that theme
implicit def input2configuredOps(i:Input):ConfiguredOpsOn[Input]
Russ - quite clearly your global-state is not "safe" in the same manner as Tony's example WiteWithContext
The other issue is that, with a global config singleton, you can have no way of knowing whether a given method needs any config access; because it is not expressed through the type system:def calculateCollisionProbability(ctx: ComputedWithContext[Context]) : DoubleQuite clearly describes what this function needs access to in order to perform its task
On Wednesday 14 September 2011, Tony Morris wrote:
> ...
>
> You're talking about program composition in general here. This is the
> very thesis of functional programming.
>
> ...
Is there anything else? Is there anything _not_ subjugated to the goal
of compositionality?
I ask this because it seems that systems like ScalaZ are so utterly,
extremely single-minded in their pursuit of this notion of composition
that every other concern has been sacrificed.
Randall Schulz
Yes. There is concession all the time.
The goal is single-minded devotion to practical application and
sacrificing from the ideal only to the extent necessary, mandated by
external influences rather by personal ignorance (which, if that
personal ignorance is the barrier, it becomes the target).
- --
Tony Morris
http://tmorris.net/
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/
iQEcBAEBAgAGBQJOdB16AAoJEPxHMY3rBz0PNkkIAKT73jkzP1ogwFjYrcgDS/i0
+Hc7G9yYyv+PeqUWvLWqLv0692CR5ApF5iryh6owpTvJtjGJtx0sC9RUcjIVPZtY
x+XcCRYAa6QdomhJdmrcAQg8fJSkOnrjvp2MpeKHYEbWLjvYIPlChNBr+ZKkcE2X
e6eI6SCgLbeloa/dZWrxgDxKkvTs8+97cOMUjK2rK/DbhiO76qZ6hyy0A2VTrXbY
puhqneohGXmKPlPqCVO8kMtcNwdkfooswTdih0IdaQBIGp/GdfnA6/g2JqVB4haQ
VsRad4I8oE8F4Se0yEfbcyhjw9tL/92RdLwlAa7ThTsZ+8mF+lMTWXB0+sm5zHU=
=TwYI
-----END PGP SIGNATURE-----
abstract class ConfigReader[A] {
def apply(config: Configuration): A
def map[B](f: A => B): ConfigReader[B] =
new ConfigReader[B] {
def apply(c: Configuration) =
f(ConfigReader.this.apply(c))
}
def flatMap[B](f: A => ConfigReader[B]): ConfigReader[B] =
new ConfigReader[B] {
def apply(c: Configuration) =
f(ConfigReader.this.apply(c))(c)
}
}
object Main {
def lift4ConfigReader[A, B, C, D, E](f: A => B => C => D => E):
ConfigReader[A] =>
ConfigReader[B] =>
ConfigReader[C] =>
ConfigReader[D] =>
ConfigReader[E] =
a => b => c => d=>
for{
aa <- a
bb <- b
cc <- c
dd <- d
} yield f(aa)(bb)(cc)(dd)
def main(args: Array[String]) {
// utility construction
def configReader[A](k: Configuration => A): ConfigReader[A] =
new ConfigReader[A] {
def apply(c: Configuration) = k(c)
}
val hostname = configReader(_.hostname)
val port = configReader(_.port)
val outfile = configReader(_.outfile)
val fullpath = configReader(_.fullPath)
val r = for {
h <- hostname
p <- port
o <- outfile
f <- fullpath
} yield "Hello there " + h + ":" + p +
"! Want to write to " + o + "?" + " Full path is: " + f
val conf =
Configuration()
println(r(conf))
}
}
Here is the Configuration.scala file:
trait BusinessRules {
def strToInt(s: String): Int = {
val i: Int = Integer.parseInt(s.toString())
i
}
def intToString(i: Int): String = {
val s: String = "" + i
s
}
}
object Configuration extends BusinessRules {
def getHostname: String = {
"localhost"
}
def port: Int = 80
def getOutfile: String = {
"/etc/hosts"
}
override def toString: String = {
"ftp://" + getHostname + ":" + port + getOutfile
}
}
case class Configuration(
hostname: String = Configuration.getHostname,
port: String = Configuration.intToString(Configuration.port),
outfile: String = Configuration.getOutfile,
fullPath: String = Configuration.toString()
)
object Configuration {
val Hostname: String = "localhost"
val port: Int = 80
val getOutfile: String = "/etc/hosts"
}
Does that allow you to change configuration parameter values between runs without recompiling?
If not, then what does all that boilerplate buy for you that you can't get withobject Configuration {
val Hostname: String = "localhost"
val port: Int = 80
val getOutfile: String = "/etc/hosts"
}
Sorry if that's a dumb question.
--Russ P.
Does that allow you to change configuration parameter values between runs without recompiling?--
If not, then what does all that boilerplate buy for you that you can't get with
object Configuration {
val Hostname: String = "localhost"
val port: Int = 80
val getOutfile: String = "/etc/hosts"
}
Sorry if that's a dumb question.
--Russ P.
http://RussP.us
It allows me to place the configuration into a .class file external from the functionality of the rest of the solution.
The business rules are free to do whatever they need to in order to apply logic as needed. That notion includes potentially reading settings from a file I suppose (which said file could be updated between executions as you mention). My use case is different and would not need that ability (at least at the present), but the functionality should be completely possible. In fact, what the business rules do is completely open ended. The case class becomes the contract ("interface" if you will), and as long as it is adhered to the sky is the limit.
def main(args: Array[String]) {
//if only Function1 had a flatMap method...
import scalaz._
import Scalaz._
//and now it does!
val hostname = (_: Configuration).hostname
val port = (_: Configuration).port
val outfile = (_: Configuration).outfile
val fullpath = (_: Configuration).fullPath
val r: Configuration => String = for {
h <- hostname
p <- port
o <- outfile
f <- fullpath
} yield "Hello there " + h + ":" + p + "! Want to write to " + o +
"?" + " Full path is: " + f
val conf = Configuration()
println(r(conf))
Absolutely nothing in that particular case. However, using a reader monad would allow you to separate the source of the configuration from its use. I.e. test vs prod etc. That's possible as your configuration receiving code is separated via the reading functions.
The difference to DI is of course the immutable nature of it. Also it could mimic scopes etc but still behave in a similar fashion (the for loop managing "scope").
That said, I am not so convinced of its practicablity - there sure is a lot of boiler plate. Accessing a singletons val is far simpler for most apps. DI imo is far worse at infecting your code.
Tony gave a presentation that covered exactly this. Whilst his emails can be difficult to process that presentation was wonderfuly easy to follow. No idea on the link right now though, but an email sent to the lists also gave the resulting code.
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
On 09/12/2011 09:38 PM, Billy wrote:
> A debate started on the scala-language group that I am moving to this
> group as it is more fitting here. The matter of DI was discussed, and
> I am actively porting the DI portions of Spring to scala to answer a
> question of practicality. In this effort, the resulting solution
> (which I have dubbed "Recoil") may end up not looking anything like
> the original framework. The requirements that spawned this are a
> matter of practicality around updating large solutions that have
> already been put into production use. My goal is to see if an object-
> functional framework can be devised that allows for zero updates to
> existing solutions so as to maintain a single codebase within a global
> scale enterprise. It is hoped that such a solution will allow for
> domain specific rules to be injected in order to meet the needs of
> multiple different domains without apriori knowledge of said rules.
>
> Billy[moving to scala-debate per agreed request]
I don't recall your constraints so cannot answer your question.
Someone (I forget who) recently wrote (I forget where): "DI is just
socially acceptable global variables." This is mostly true -- I say
mostly because I think the adverb "globally" is redundant and
misleading. That is to say, there is no such thing as a global variable.
All variables are scoped to some context and it is the extent of this
context that is a measure of detriment. This is why you hear people
talking about "keeping their side-effects local." This wishful-thinking
almost never eventuates because side-effects are pervasive. I am
side-tracking here, but going back to the original topic briefly.In the absence of my awareness of your constraints, I can point out what
it is that most people want when they think they want DI, and in fact,
do not, ever (it is one of many forms of masochism in programming --
bare with me).First, let us consider a general Scala program and generalise it. This
is just an arbitrary program -- I am trying to make it as convoluted as
possible so that you can go back to a real program and apply the same
reasoning. Importantly, this program is side-effect free at this point.val a = e1
val b = e2(a)
val c = e3(a, b)
val d = e2(b)OK, now I am going to generalise it by running the same program, but in
a for-comprehension. We do this by following these rules:
1) Remove the 'val' keyword
2) The = symbol becomes <-
3) We wrap the program in for and yieldI am going to create a data type that simply wraps a value and provides
flatMap and map methods so I can do this:case class Id[A](i: A) {
def map[B](f: A => B) = Id(f(i))
def flatMap[B](f: A => Id[B]) = f(i)
}...and since I don't want to explicitly wrap/unwrap my values with Id, I
am going to provide an implicit for in and out:object Id {
implicit def IdIn[A](a: A) = Id(a)
implicit def IdOut[A](a: Id[A]) = a.i
}OK, so now let's translate our program:
for {
a <- e1
b <- e2(a)
c <- e3(a, b)
d <- e2(b)
} yield d
Now that you accept that any program can be written this way, let us
step away for a moment and address the idea of DI. There are usually two
variations on DI:
1) The "configuration" (or context) is done and the application must
start by first initialising this context, then the application may run.
The application then reads from the configuration during run-time but
does not modify it. If this order is altered, you end up with a broken
program. A "DI" container attempts to promise you that no such thing
will occur -- this is essentially what the selling point is.This dependency on explicit execution order is directly anti-thetical to
the functional programming thesis. This is a consequence of there being
a widely-scoped variable that kind-of pretends otherwise.If you turn your head just a little, you can see this is a somewhat
degenerate notion of what is called "uniqueness typing." I digress.2) Same as above, however, not only is the application permitted to read
the configuration, but it is also permitted to *write* to it. This means
that the application depends on *more* explicit execution order and the
possibility of bugs increases even more.Imagine if I said, "you know what, turn all that DI stuff off, we are
going to initialise our values up front and pass them all the way
through the application." You would surely protest, "but that is so
clumsy!" and you'd be right, but only at first glance.
How do we deal with read and write values (case 2)? Well, we need a new
different data type for that:case class WriteWithContext[A](cx: Context => (A, Context))
Notice how this is the same data type as before except the function can
now produce a *new* Context as well as the computed value (paired). This
is to say, we may "modify" the Context as it is threaded through. But
what about map and flatMap, can we write those? Of course:case class WriteWithContext[A](cx: Context => (A, Context)) {
def map[B](f: A => B): WriteWithContext[B] = WriteWithContext(c => val
(a, cc) = cx(c); (f(a), cc))
def flatMap[B](f: A => WriteWithContext[B]): WriteWithContext[B] =
WriteWithContext(c => { val (a, cc) = cx(c); f(a) cx cc })
}Don't get too carried away with reading those methods, but just note
that flatMap "threads the Context through whatever the function is,
which may be modifying it."OK, so now if we suppose that our expressions (e1, e2, e3) actually had
access to the Context, but were also able to "modify" it by returning a
new Context (or just leaving it alone, for which there is library
support of course), then our program would look like this:
for {
a <- e1
b <- e2(a)
c <- e3(a, b)
d <- e2(b)
} yield d
Yep, exactly the same as before. So now we have a value that we can pass
in a Context and it is threaded through the program, potentially
"modifying" the Context as it is threaded through and we get a value and
the resulting Context at the end. We may wish to drop either of these --
in practice, the Context often gets dropped, since it was only need to
compute the value -- and of course, there is library support for that.
So hopefully now you see that DI can be replaced by a superior
programming model, at least for this example, and I promise, for any
example. We just have to come to terms with a few data types and
abstractions and we can kick that baby to the gutter where it belongs.Hope that helps!
- --
Tony Morris
http://tmorris.net/-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/
iQEcBAEBAgAGBQJObfLOAAoJEPxHMY3rBz0Ps/oH/iXV+fEk9hGdUN0TbdcqR3Fh
QlwVc3VN9El4jLb2yQdopL+93Tov3EYn1beVcC1R7ptI75jtrmRcppPMaJdRUFnN
jeDvqXghac0+evt8zoaK2GqGq1H3R8eG6kdx5pBjf+0PCiJS9RziRQpITb+5Kob2
0I6MSe+beNe7UcVX7HGp9oGVx56CTaieh2R+H6LtGlhwahh//6BBeEyLflIGdc4w
8O8oJwfxT5lnsn2aXsxveB+zkywNyl+dxPEk83o5E3AVIKCvaEnRTKmwd4LsHIQM
D/KVVyaPSKyvziEaVNwAlsukC3LoYg+MBhq9jRAJ65wCEhB+M8oUHNjgnDDbloA=
=J7D1
-----END PGP SIGNATURE-----