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(¬(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 ¬(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.