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.