Dependency Injection ๐ in Forth ๐ โโโโโโโโโโโโโโโโโโโโ May 27, 2023 Introduction โโโโโโโโโโโโ โผ What is Dependency Injection? Why? โผ OOFda, yet another Forth OO system โผ Poke, a Forth Dependency Injection tool โผ Can we make this simpler? ๐ค What is Dependency Injection? โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โผ Inversion of Control โผ ๐ Hollywood Principle ๐ฅ "Don't call us..." โผ Dependencies are passed in, not directly instatiated โผ "$25 term for a 5ยข concept" Where is it used? โโโโโโโโโโโโโโโโโ โผ Most popular in Java, Kotlin, and C# โผ Probably popular in these strongly typed languages, because: - Weak module (file) runtime scope - Typing of closures is hard - Few monkeypatching options ๐ - Lots of boilerplate public class Foo { private final Bar bar; private final Baz baz; Foo() { this.bar = new Bar(); this.baz = new Baz(); } ... } Foo foo = new Foo(); public class Foo { private final Bar bar; private final Baz baz; Foo(Bar bar, Baz baz) { this.bar = bar; this.baz = baz; } ... } Foo foo = new Foo(new Bar(), new Baz()); Why Dependency Injection? โโโโโโโโโโโโโโโโโโโโโโโโโ โผ Improve testing, inject mocks and fakes โผ Ease refactoring ๐๏ธ โผ Swap in alternate variant components โผ Can be made easier with frameworks ๐ฆ โผ Allow powerful mix-ins ๐ธ โผ Encourage encapsulation โผ Enable lego-block programming style ๐งฑ @coffee_maker.png@thermosiphon.png
@electric_heater.png
@coffee_all.png
@pountain.jpg
Object Oriented Forth โโโโโโโโโโโโโโโโโโโโโ โผ Forth allows infinite diversity in infinite combinations ๐ โผ Pountain, 1986 โผ In 1996, Brad Rodrigeuz examined 17! different OO Forth systems โผ SWOOP is nice ๐ซก CLASS POINT VARIABLE X VARIABLE Y : SHOW ( -- ) X @ . Y @ . ; : DOT ( -- ) ." Point at " SHOW ; END-CLASS But... I'll make my own OOFda โโโโโ โผ Every Object starts with a pointer to a vtable โผ Classes are objects that include a vtable โผ Fixed size vtables โ๐ฎโ โผ Method are words that invoke vtable offsets โผ Data fields are accessible only at definition time โผ All methods are public + dynamic โผ Single Inheritance, no interfaces required ๐ฆ @vtable.png
class Point2 variable x variable y : .construct ( x y -- ) y ! x ! ; : .show x @ . y @ . ; : .dot ." Point at " this .show ; end-class class Point2 value x value y : .construct ( x y -- ) to y to x ; : .show x . y . ; : .dot ." Point at " this .show ; end-class class Point3 extends Point2 value z : .construct ( x y z -- ) to z super .construct ; : .show super .show z . ; end-class 1 2 3 Point3 .new constant p p .show => Point at 1 2 3 @points.png
class Foo value bar value baz : construct ( bar baz -- ) to baz to bar ; ... end-class Bar .new Baz .new Foo .new constant f Implementation โโโโโโโโโโโโโโ โผ Pick a fixed method limit ๐ฏ โผ Use a global for "this" ๐ โผ Keep most things in a "classing" vocabulary โผ Methods are alway in the "FORTH" vocabulary defined? oofda-max-methods 0= [IF] 100 constant oofda-max-methods [THEN] vocabulary classing also classing definitions also forth variable 'this : this ( -- o ) 'this @ ; variable methods variable last-method : new-method ( ".name" -- xt ) methods @ oofda-max-methods >= throw create methods @ , 1 methods +! latestxt does> this >r swap ( save this ) 'this ! ( switch it ) dup last-method ! ( save last method ) @ cells this @ + @ execute ( invoke method ) r> 'this ! ( restore this ) ; : method ( ".name" -- xt ) current @ >r also forth definitions >in @ bl parse find dup if nip else drop >in ! new-method then previous r> current ! ; : m# ( "name" -- n ) method >body @ ; : m: ( "name" ) method drop ; m: .construct ( make this 0 ) : m! ( xt n class ) swap 3 + cells + ! ; m: .fallback : undefined last-method @ 2 cells - ( body> ) this .fallback ; : error-fallback ( xt -- ) ." Undefined method: " >name type cr throw -1 ; : blank-vtable oofda-max-methods 0 do ['] undefined , loop ; create ClassClass here 3 cells + , ( vtable* ) 0 , ( parent ) oofda-max-methods 3 + cells , ( size ) blank-vtable ( vtable[] ) : nop-construct ; m: .size m: .grow m: .vtable m: .parent m: .getClass :noname ( xt n ) this m! ; m# .setMethod ClassClass m! : create ( "name" ) create this .size , does> @ this + ; : variable ( "name" ) create this .size , cell this .grow does> @ this + ; : value ( "name" ) create this .size , cell this .grow does> @ this + @ ; : field' ( "name" -- n ) ' >body @ ; : to ( n -- "name" ) field' postpone literal postpone this postpone + postpone ! ; immediate : +to ( n -- "name" ) field' postpone literal postpone this postpone + postpone +! ; immediate : dosuper ( n -- ) this ClassClass .getClass .parent .vtable + @ execute ; : super ( "method" ) field' cells postpone literal postpone dosuper ; immediate : : ( "name" ) m# :noname ; : ; postpone ; swap this .setMethod ; immediate : defining ( cls -- ) 'this ! current @ also classing definitions ; m: .new m: .inherit : class create ClassClass .new defining ; : end-class previous current ! 0 'this ! ; : extends ' execute this .inherit ; : extend ' execute defining ; : ClassClass ( -- cls ) ClassClass ; extend ClassClass : .parent ( -- a ) this cell+ @ ; : .setParent ( a -- ) this cell+ ! ; : .size& ( -- a ) this 2 cells + ; : .size ( -- n ) this .size& @ ; : .setSize ( -- n ) this .size& ! ; : .grow ( n -- ) this .size + this .setSize ; : .vtable ( -- a ) this 3 cells + ; : .getClass ( o -- cls ) @ 3 cells - ; : .allocate ( n -- a ) here swap allot ; : .getName ( -- a n ) this 2 cells - >name ; : .getMethod ( n -- xt ) cells this .vtable + @ ; : .construct 0 this .setParent cell this .setSize oofda-max-methods 0 do ['] undefined i this .setMethod loop ['] error-fallback [ m# .fallback ] literal this .setMethod ['] nop-construct [ m# .construct ] literal this .setMethod ; : .setup ( -- cls ) this .size this .allocate dup this .size 0 fill this .vtable over ! ; : .new ( -- cls ) this .setup dup >r .construct r> ; : .inherit ( cls -- ) dup this .setParent .size& this .size& oofda-max-methods 1+ cells cmove ; end-class Back to DI... Making DI Easier โโโโโโโโโโโโโโโโ โผ Hooking things up manually is tedious ๐ โผ Doing setup globally is unscalable โ โผ Could we make it automatic + declarative? DI Frameworks โโโโโโโโโโโโโ โผ Spring ๐ฑ โผ Guice ๐ง โผ Dagger ๐ก๏ธ โผ Dagger 2 ๐ก๏ธ๐ก๏ธ Spring ๐ฑ โโโโโโโโโ โผ Specify dependencies in XML ๐ค - Config doesn't live with code :-( โผ Runtime validation + configuration ๐ <beans> <bean id="coffeeMaker" class="CoffeeMaker"> <constructor-arg ref="heater"/> <constructor-arg reg="pump"/> <bean> <bean id="heater" class="ElectricHeater"> <constructor-arg reg="pump"/> </bean> <bean id="pump" class="Thermosiphon"/> </beans> Java Annotations โโโโโโโโโโโโโโโโ โผ Include compile/runtime code annotations โผ Syntax aware extensions to language ๐จ โผ Compile time code generators that add classes โผ A problem Forth doesn't really have! - See IMMEDIATE ๐ public @interface MyAnnotation { } @MyAnnotation public void method() { } public @interface MyAnnotation { String name(); int number(); } @MyAnnotation(name = "Rumplestitskin", number = 123) MyClass object = new MyClass(); @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface MyAnnotation { } ๐ง Guice & Dagger ๐ก๏ธ โโโโโโโโโโโโโโโโโโโโโโ โผ Guice - Annotation based configuration ๐ง Runtime validation โผ Dagger 1 - Partial compile time validation Runtime graph composition ๐ก๏ธ โผ Dagger 2 - ๐ก๏ธ๐ก๏ธ Compile time validation /** A coffee maker to brew the coffee. */ public class CoffeeMaker { private final CoffeeLogger logger; private final Lazy
heater; // Create a possibly costly heater only when we use it. private final Pump pump; @Inject CoffeeMaker(CoffeeLogger logger, Lazy heater, Pump pump) { this.logger = logger; this.heater = heater; this.pump = pump; } public void brew() { heater.get().on(); pump.pump(); logger.log(" [_]P coffee! [_]P "); heater.get().off(); } } DI Framework Concepts โโโโโโโโโโโโโโโโโโโโโ โผ @Inject - Ask for dependencies (and sometimes provide) Used in regular code โผ @Provides - Provide ambiguous bindings Used in modules โผ @Module - A "glue" class to contain specializations (e.g. a thermosiphon as a pump) โผ @Component - Container for modules or implicit modules Generated into an instantiable thing โผ @Singleton - Global scope annotation (other scopes exist) Poke โโโโ โผ Build DI-framework-like tools on OOFda ๐๏ธ โผ Simplify framework concepts ๐ซข โผ Use strings for bindings ๐งถ โผ Explicit @Inject for each needed dependency ๐ โผ Explicit .providesName methods for providings bindings โผ Component base class to create module container โผ Runtime on demand instantiation ๐ ( A coffee maker to brew the coffee. ) class CoffeeMaker value logger value heater value pump m: .provideCoffeeLogger m: .provideHeater m: .providePump m: .on m: .off m: .pump m: .isHot? : .construct @Inject CoffeeLogger to logger @Inject Heater to heater @Inject Pump to pump ; : .brew heater .on pump .pump s" [_]P coffee! [_]P " logger .log heater .off ; end-class class CoffeeMakerModule : .provideCoffeeMaker CoffeeMaker .new ; end-class ( A logger to log steps while brewing coffee. ) class CoffeeLogger value logs : .construct 30 Array .new to logs ; : .log ( a n -- ) String .new logs .append ; : .dump logs .length 0 ?do i logs .get .get type cr loop cr ; end-class class LoggerModule : .provideCoffeeLogger @Singleton CoffeeLogger .new ; end-class ( An electric heater to heat the coffee. ) class ElectricHeater value logger value heating : .construct @Inject CoffeeLogger to logger 0 to heating ; : .on -1 to heating s" ~ ~ ~ heating ~ ~ ~" logger .log ; : .off 0 to heating ; : .isHot? ( -- f ) heating ; end-class class HeaterModule : .provideHeater @Singleton ElectricHeater .new ; end-class ( A thermosiphon to pump the coffee. ) class Thermosiphon value logger value heater : .construct @Inject CoffeeLogger to logger @Inject Heater to heater ; : .pump heater .isHot? if s" => => pumping => =>" logger .log then ; end-class class PumpModule : .providePump Thermosiphon .new ; end-class ( The main app responsible for brewing the coffee and printing the logs. ) class CoffeeApp extends Component : .construct super .construct HeaterModule this .include PumpModule this .include LoggerModule this .include CoffeeMakerModule this .include ; end-class CoffeeApp .new constant coffeeShop coffeeShop .provideCoffeeMaker .brew coffeeShop .provideCoffeeLogger .dump Implementation โโโโโโโโโโโโโโ โผ Use a global to pass a provider through constructors - Keep an array of providing modules - save the current one if we nest ๐ชบ โผ Use IMMEDIATE magic for @Singleton to store a cached values inside modules objects data area โผ Use .fallback handler for undefined methods to route to each provider module in turn ๐ฒ โผ Count how many providers to avoid bad graphs class Array value data value length value capacity : .construct ( n -- ) to capacity here to data capacity cells allot 0 to length ; : .get ( n -- n ) cells data + @ ; : .set ( n n -- ) cells data + ! ; : .length ( -- n ) length ; : .capacity ( -- n ) capacity ; : .append ( n -- ) this .length this .capacity >= throw this .length this .set 1 +to length ; : .length ( -- n ) length ; end-class variable provider : do@Inject ( xt -- o ) provider @ swap execute ; create name-buffer 200 allot 0 value name-length : 0name 0 to name-length ; : +name ( a n -- ) dup >r name-buffer name-length + swap cmove r> +to name-length ; : name ( -- a n ) name-buffer name-length ; : @Inject ( "name" -- o ) 0name s" [ ' .provide" +name bl parse +name s" ]" +name name evaluate postpone literal postpone do@Inject ; immediate : do@Singleton ( n -- n ) this + dup @ if @ rdrop exit then r> swap [ here 7 cells + ] literal swap >r >r >r exit r> over >r ! r> ; : @Singleton this .size postpone literal cell this .grow postpone do@Singleton ; immediate class Component value providers : .construct 50 Array .new to providers ; : .include ( m -- ) .new providers .append ; : .hasMethod ( m n -- f ) providers .get ClassClass .getClass .getMethod ['] undefined <> ; : .countHasMethod { m -- f } 0 providers .length 0 do m i this .hasMethod if 1+ then loop ; : .pickHasMethod { m -- provider } 0 providers .length 0 do m i this .hasMethod if i providers .get unloop exit then loop -1 throw ; : .fallback { xt } xt >body @ { m } provider @ { old } this provider ! m this .countHasMethod { matches } matches 1 > if ." Multiple Providers: " xt >name type cr -1 throw then matches 1 <> if xt error-fallback then m this .pickHasMethod xt execute old provider ! ; end-class But is it Forthy? โโโโโโโโโโโโโโโโโ โผ This is a LOT of complexity! (Even in Java!) ๐คช โผ Singleton vs N is partially the cause โผ Forth thrives on singletons ๐คซ โผ ๐ Forth has a better mechanism! DEFER / IS DEFER log DEFER heater-on DEFER heater-off DEFER pump : brew heater-on pump s" [_]P coffee! [_]P " log heater-off ; : console-log ( a n -- ) type cr ; ' console-log IS log DEFER hot? : thermosiphon hot? if s" => => pumping => =>" log then ; ' thermosiphon IS pump 0 value switch : electric-on -1 TO switch s" ~ ~ ~ heating ~ ~ ~" log ; : electric-off 0 TO switch ; ' electric-on IS heater-on ' electric-off IS heater-off ' switch IS hot? brew โ ~ ~ ~ heating ~ ~ ~ => => pumping => => [_]P coffee! [_]P https://github.com/flagxor/ueforth /tree/main/attic/oofda QUESTIONS? ๐ Thank you!