Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add S35_Reformat by reusing S33_DISASSEMBLE and S34_ASSEMBLE #4

Merged
merged 4 commits into from
Jan 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/CspExamples.jl
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,83 @@ function S34_ASSEMBLE(X::Channel{Char}, lineprinter::Channel{>:String}, lineleng
end
end

"""
S35_Reformat(west::Channel{String}, east::Channel{>:String}, linelength=125)

3.5 Reformat

> "Problem: Read a sequence of cards of 80 characters each, and print
the characters on a lineprinter at 125 characters per line. Every card
should be followed by an extra space, and the last line should be
completed with spaces if necessary."

This one's fun! We can reuse the existing functions by creating an intermediate Channel and
Task (equivalent to a Process in Hoare's paper) to act as the output and then input.

Note that this function blocks on its call to ASSEMBLE, so it doesn't return until the
reformatting is completed. This keeps with the style of the functions seen so far, which
block internally, allowing the caller to choose whether to run it asynchronously.
"""
function S35_Reformat(west::Channel{String}, east::Channel{>:String}, linelength=125)
# This Channel constructor creates a channel and spawns a Task (and yields to it). When
# the Task is completed, the Channel is closed.
disassembled = Channel(ctype=Char) do ch
S33_DISASSEMBLE(west, ch)
end
S34_ASSEMBLE(disassembled, east, linelength)
end

"""
S35_Reformat2(west::Channel{String}, east::Channel{>:String}, linelength=125)

An alternative implementation of S35_Reformat, which makes the concurrency operations more
explicit, and keeps a style more similar to Go.
"""
function S35_Reformat2(west::Channel{String}, east::Channel{>:String}, linelength=125)
tmp = Channel{Char}(0)
@async begin
S33_DISASSEMBLE(west, tmp);
# Note that we must close(tmp) to signal to ASSEMBLE that it's okay to return,
# because we've chosen to not have DISASSEMBLE and ASSEMBLE close output channels.
close(tmp);
end
S34_ASSEMBLE(tmp, east, linelength)
end

"""
S35_Reformat_non_concurrent(input::Array{String})::Array{String}

As Hoare notes in the paper:
> "This elementary problem [Reformat] is difficult to solve elegantly without coroutines."

This is indeed the case, as illustrated in this best-faith attempt. What makes it tricky is
that you have two separate, but overlapping, loops that end at different times. You cannot
represent them both as real for-loops in straightline code, but you can in concurrent
programming, because a concurrent program grants you two separate Instruction Pointers to
trace through each loop.

Here, without concurrency, we emulate the second loop (printing 125-character lines) by
with an if-statement, and manually re-initializing the loop's variables.
"""
function S35_Reformat_non_concurrent(input::Array{String}, linelength=125)::Array{String}
outs = String[]
out = Char[]
for s in input
for c in s
push!(out, c)
# This is the manual implementation of the "second loop":
if length(out) == linelength
# "end of the loop"
push!(outs, rpad(String(out), linelength))
out = Char[] # Re-initialize
end
end
push!(out, ' ')
end
# Here we have to duplicate the end of the "second loop" since there are two ways to
# exit the "loop condition".
push!(outs, rpad(String(out), linelength))
return outs
end

end
23 changes: 23 additions & 0 deletions test/CspExamples.jl
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,26 @@ end
expected = [String(expected[1+(i-1)*linelength:i*linelength]) for i in 1:2]
@test collect(lineprinter) == expected
end

@testset "S35_Reformat" begin
reformatted = Channel() do ch
CspExamples.S35_Reformat(make_filled_channel(["hello", "world"]), ch, 4)
end
@test collect(reformatted) == ["hell", "o wo", "rld "]
end
@testset "S35_Reformat2" begin
reformatted = Channel() do ch
CspExamples.S35_Reformat2(make_filled_channel(["hello", "world"]), ch, 4)
end
@test collect(reformatted) == ["hell", "o wo", "rld "]
end
@testset "S35_Reformat_non_concurrent" begin
# While the implementation of S35_Reformat_non_concurrent is more convoluted than
# the concurrent version, the callsite is of course simpler because we don't have to
# consider concurrency at all.
#
# A best-of-both-worlds approach might be to expose a "normal" interface like this one,
# as a wrapper around a concurrent implementation.
reformatted = CspExamples.S35_Reformat_non_concurrent(["hello", "world"], 4)
@test reformatted == ["hell", "o wo", "rld "]
end