72: Functional Core, Imperative Shell


The content

To get started, we the example class we start with is a Tweet class.

require 'values' class Tweet < Value.new(:tid, :username, :text) def initialize(tid, username, text) super(tid.to_i, username, text) end def line_count lines.count end def lines text.split("\n") end end # Another file class Timeline attr_reader :tweets def initialize(tweets) @tweets = tweets end def add(tweets) existing_ids = @tweets.map(&:tid).to_set new_tweets = tweets.reject { |t| existing_ids.include?(t.tid) } Timeline.new(new_tweets + @tweets) end end

There is an even more Cursor class that is explained in the video, but in the video he is happy that Cursor is immutable.

As a last example of the functional core, there is a TweetRenderer class.

They are all examples of the "functional core". The only difference is the re-binding of local variables.

The imperative shell will be encapsulated in a single file (mostly at least) that manages the work like updating the database, etc. This is a file that deals with side-effects.

An example of an imperative shell:

class Toot def initialize(screen) @screen = screen end def run TwittlerLib.authenticate database = Database.default timeline = Timeline.new(database.timeline) timeline_queue = SelectableQueue.new # start_timeline_stream(timeline, timeline_queue) world = World.new(timeline) view = TimelineView.new(@screen) EventLoop.new(database, world, timeline_queue, view).run end def start_timeline_stream(timeline, timeline_queue) timeline_stream = TimelineStream.new(timeline, timeline_queue) Thread.new { timeline_stream.run } end end

Note: the entire outer shell contains NO tests. He also mentions that he cowboy codes a little bit in the outer shell, and once he knows what he wants to do then he will move the source code into unit-tested files using TDD.