FPOO Chapter 7: Programming with Dataflow

[boilerplate bypath=”fp-oo”]

This is where FPOO starts to get interesting. I have to say I find the choice of the term “dataflow” confusing due to its other associations in programming. But this section introduces a style of working with data–first annotating it, then filtering it–which I’ve never really given a lot of thought to.

I’m going to start out by defining a record type for courses. Marick doesn’t do this for his example, but Clojure has more shortcuts for working with raw maps (e.g. being able to use a key as a function of the map). I’m curious if this kind of dataflow programming is easy to adapt to records.

defrecord Course, course_name: nil, morning?: true, limit: Infinity, registered: 0

Let me just make sure I know how to use records.

test "Course" do
  alias FpOoElx.Exercises.Scheduling.Course
  c = Course[course_name: "Zigging", morning?: true, limit: 5, registered: 3]
  assert c.course_name == "Zigging"

  # Or the more functional style attribute access
  import Course
  assert morning?(c) == true

  assert c.to_keywords ==
    List.keysort([course_name: "Zigging", morning?: true, limit: 5, registered: 3], 0)

  c2 = c.limit(10)
  assert c2.limit == 10
end

OK, now on to the first metadata-annotating function.

def answer_annotations(courses, registrants_courses) do
  checking_set = registrants_courses
  courses |> map fn(course)->
    course_attrs = course.to_keywords
    course_attrs |> merge(
      spaces_left: course.limit - course.registered,
      already_in?: checking_set |> Enum.member?(course.course_name))
  end
end
test "answer_annotations" do
  alias FpOoElx.Exercises.Scheduling.Course
  import Enum
  courses = [Course[course_name: "zigging", limit: 4, registered: 3],
             Course[course_name: "zagging", limit: 1, registered: 1]]
  annotated = courses |> answer_annotations(["zagging"])
  assert at(annotated, 0)[:already_in?] == false
  assert at(annotated, 0)[:spaces_left] == 1
  assert at(annotated, 1)[:already_in?] == true
  assert at(annotated, 1)[:spaces_left] == 0
end

And now the second. This one differs from the first in that it assumes it will get keyword lists instead of a records. This is a strike against using records in the first place, since now these two functions differ in this seemingly arbitrary way.

def domain_annotations(courses) do
  courses |> map fn(course)->
    course |> merge(
      empty?: course[:registered] == 0,
      full?:  course[:spaces_left] == 0)
  end
end
test "domain_annotations" do
  import Enum
  annotated = [[registered: 1, spaces_left: 1],
               [registered: 0, spaces_left: 1],
               [registered: 1, spaces_left: 0]] |> domain_annotations
  assert at(annotated, 0)[:full?] == false
  assert at(annotated, 0)[:empty?] == false
  assert at(annotated, 1)[:full?] == false
  assert at(annotated, 1)[:empty?] == true
  assert at(annotated, 2)[:full?] == true
  assert at(annotated, 2)[:empty?] == false
end

And now the final annotation function, which adds notes on course availability.

def note_unavailability(courses, instructor_count) do
  out_of_instructors? = 
    instructor_count ==
    (courses |> filter(&not(empty?(&1))) |> count)
  courses |> map fn(course) ->
    course |> merge(
      unavailable?: course[:full?] || (out_of_instructors? && course[:empty?]))
  end
end

I’m pleasantly surprised I can use the capture operator for the nested &not(empty?(&1)) expression.

Finally, the payoff. At this point the book introduces the arrow (->) operator for threading functions together, but of course this is Elixir so we do that all the time.

def annotate(courses, registrants_courses, instructor_count) do
  courses |> answer_annotations(registrants_courses)
          |> domain_annotations
          |> note_unavailability(instructor_count)
end

I’m tired of translating now, but I’m going to do one quick check that this works as expected.

test "annotate" do
  import Enum
  alias FpOoElx.Exercises.Scheduling.Course
  courses = [
    Course[course_name: "zigging", limit: 4, registered: 3],
    Course[course_name: "zagging", limit: 1, registered: 1]
  ]
  registrants_courses = ["zigging"]
  instructor_count = 2
  annotated = courses |> annotate(registrants_courses, instructor_count)
  assert at(annotated, 0)[:unavailable?] == false
  assert at(annotated, 1)[:unavailable?] == true
end

(I’d really like to find a way to avoid having to explicitly alias the Course type, and instead have it show up when importing the Scheduling module it lives in.)

This has been instructive, but time-consuming. Enough for now.

Leave a Reply

Your email address will not be published. Required fields are marked *