From cf78dc036c300ed836a0097d3915e7d14f173f01 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Thu, 1 Dec 2022 00:01:54 -0800 Subject: [PATCH] Thorouhgly comment day 1 solution in Chapel --- day1.chpl | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 3 deletions(-) diff --git a/day1.chpl b/day1.chpl index cf22012..8f70a77 100644 --- a/day1.chpl +++ b/day1.chpl @@ -1,11 +1,28 @@ use IO; use List; +/* + The numbers come to us in blank-line-separated groups. The easiest way to + process all of these groups is to keep an intermediate accumulator + that represents the total number within a group, record that accumulator + each time we hit an empty line. On the other hand, the last group is + not terminated by an empty line, so we'd need special logic to handle + that case. Unless, of course, we just pretended there's an empty line + at the end, too. We can do this with a custom iterator, `linesWithEnding`. +*/ iter linesWithEnding() { for line in stdin.lines() do yield line; yield ""; } +/* + On to the actual intermediate accumulator logic described above. The + `current` variable will keep the "up-to-this-point" total within a group. + Whenever we hit an empty line, we know we've finished processing a group, + so we report the value of `current`. Once again we'll make this logic + an iterator; each time it finishes up with a group, it will yield the + group's sum. +*/ iter elves() { var current = 0; for line in linesWithEnding() { @@ -19,18 +36,58 @@ iter elves() { } } +/* + At this point, part 1 can be solved simply as: + + ```Chapel + writeln(max reduce elves()); + ``` +*/ + +/* + For part 2, I'm going to do something a bit more unusual. Chapel has support + for reduction expressions, which can even be run in parallel over many + threads. I'll implement picking the top three elements as a + custom reduction. If I implement all the methods on this reduction + class, I'll be able to automatically make my code run on multuple threads! +*/ class MaxThree : ReduceScanOp { + /* Reductions have an element type, the thing-that's-being-processed. + This element type is left generic to support reductions over different + types of things. */ type eltType; + /* The value our reduction is building up is a top-three list of the largest + numbers. This top-three list is represented by a three-element tuple + of `eltType`, written as `3*eltType`. */ var value: 3*eltType; + /* Reductions need an identity element. This is an element that doesn't + do anything when processed. For instance, for summing, the identity + element is zero (adding zero to a sum doesn't change the sum). For + finding a product, the identity element is one (multiplying by one + leaves the product intact). When finding the _largest_ three numbers + in a list, the identity element is three [infinums](https://en.wikipedia.org/wiki/Infimum_and_supremum) + of that list. We'll assume that the default value of the `eltType` + is its infinum, which means default-initializing a tuple of three + `eltTypes` will give us such a three-infinum tuple. + */ proc identity { var val: value.type; return val; } + /* + Next are accumulation functions. These describe how to combine partial + results from substs of the list of numbers, or how to update the top + three given a new number. We only need to _really_ implement one version of + these functions - one that combines two 3-tuples. The rest can be defined + in terms of that function. + */ proc accumulate(x: eltType) { accumulateOntoState(value, x); } proc accumulateOntoState(ref state: 3*eltType, x: eltType) { accumulateOntoState(state, (0, 0, x)); } proc accumulate(x: 3*eltType) { accumulateOntoState(value, x); } + /* The accumulation function uses a standard algorithm for merging two sorted + lists. */ proc accumulateOntoState(ref state: 3*eltType, x: 3*eltType) { var result: state.type; var ptr1, ptr2: int = 3-1; @@ -48,23 +105,40 @@ class MaxThree : ReduceScanOp { proc combine(other: MaxThree(eltType)) { accumulate(other.value); } + + /* The Chapel reduction feature requires a couple of other methods, + which we implement below. */ proc clone() return new unmanaged MaxThree(eltType=eltType); proc generate() return value; } +/* + Let's make it possible to select which part we want to solve from the + command line. This can be easily achieved via a `config const`. We'll + also add a `config const` to enable/disable parallel computation. */ config const part = 1; config const parallel = false; +/* Here's how we use our solution. */ if part == 1 { + /* For part 1, the code remains the same, since we're still just finding + the one maximum number. */ writeln(max reduce elves()); } else if part == 2 { - if parallel { + if !parallel { + /* For the non-parallel version, we can just use `MaxThree` in the + reduce expression, which gives us our top-3 tuple. To solve + the puzzle, all that's left is to sum the elements of that tuple, + which we achieve via another `+ reduce`. */ writeln(+ reduce (MaxThree reduce elves())); } else { - // Parallel + /* For the parallel case, we have to use a `forall` loop, which is + Chapel's way of expressing parallelism. `forall` loops have + support for `reduce expression`. In all, the code looks like the + following. */ var max3 = (0,0,0); // Need to read all the numbers into memory to make sure we can distribute - var elfList = new list(elves()); + var elfList = elves(); // To make a reduction parallel, we use a forall loop with a reduce intent forall elf in elfList with (MaxThree(int) reduce max3) { max3 reduce= elf; @@ -72,3 +146,4 @@ if part == 1 { writeln(+ reduce max3); } } +