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

Exercise tree #560

Merged
merged 5 commits into from
Jun 29, 2018
Merged

Exercise tree #560

merged 5 commits into from
Jun 29, 2018

Conversation

coriolinus
Copy link
Member

@coriolinus coriolinus commented Jun 20, 2018

Organize exercises into a tree structure. Contributes to #543. Closes #552 by completion and closes #548 and closes #547 by exclusion.

Individual commits contain the reasoning about individual exercises. The general approach was to group them into topics, and have the easiest member introduce a topic.

Due to the large number of individual topics chosen, we ended up with a moderately high number of core exercises. I believe that this number is not excessive, but am very willing to discuss the arrangement. This is a draft, but I do not harbor the delusion that it is perfect.

All non-deprecated exercises are either core or depend on another exercise:

$ jq '.exercises[] | select((.deprecated|not) and (.core|not) and (.unlocked_by|not))' config.json

Core exercises:

$ jq '.exercises[] | select((.deprecated|not) and .core) | .slug' config.json
"hello-world"
"pythagorean-triplet"
"saddle-points"
"clock"
"luhn"
"atbash-cipher"
"bracket-push"
"sublist"
"space-age"
"macros"
"poker"
"anagram"
"minesweeper"
"parallel-letter-frequency"
"forth"
$ jq '.exercises[] | select((.deprecated|not) and .core) | .slug' config.json | wc -l
15

@coriolinus
Copy link
Member Author

This also eliminates the twofer exercise entirely, and should therefore close #552 and #548.

@coriolinus
Copy link
Member Author

Turns out that the required structure is a vine, not a tree. Who knew?

I'll rework these later.

@coriolinus coriolinus changed the title Exercise tree WIP: Exercise tree Jun 20, 2018
Just going to take bites at the general ordering problem here.
We have 19 difficulty 1 problems, and ~80 total problems, so we
want two of our difficulty 1 problems to be core.

How, then to organize the 19?

I did it like this:

Hello world is the root of the tree, the first core exercise. Once
complete, it branches into three topic areas: strings, math, and
control flow.

Raindrops is the first stringsy problem. Once complete, the rest of
the strings problems are unlocked: raindrops, reverse-string, bob,
proverb.

Gigasecond is the first math problem. Once complete, the rest of the
math problems are unlocked: leap, nth-prime, grains, prime-factors,
sum-of-multiples, armstrong-numbers.

Beer-song is the first control flow problem. Once complete, the other
one, difference-of-squares, is also unlocked.

The second core exercise is pythagorean-triplets. This was chosen
because it's simple enough to be quick to implement a naive version,
but there's a lot of potential for optimization. Once complete, it
unlocks the rest of the more challenging level 1 problems: series,
diffie-hellmann, and collatz-conjecture.
Rough tree heads:

- saddle-points: iterators
- say: algorithms
- clock: data structures
- luhn: algorithms
- atbash-cipher: ciphers
- bucket-push: use a trait
- space-age: implement a trait
- macros: macros
- sublist: generics
There were a few core problems here from which the others derived:

- poker: algos / search
- anagram: lifetimes
- minesweeper: complex structs / state
- parallel-letter-freq: concurrency
- forth: compilers/interpreters
… by core

The idea was simple: for every exercise, while its parent is not
core, update its parent to its grandparent.

This was implemented with the following script (python3.6):

```python3
import json

def load():
    with open('config.json', 'r') as fp:
        return json.load(fp)

def save(cfg):
    with open('config.new.json', 'w') as fp:
        return json.dump(cfg, fp, indent=2)

def update_exercises(exercises):
    slugs = {e['slug']: e for e in exercises}
    new_ex = []
    for e in exercises:
        if e['core']:
            print('{} is core'.format(e['slug']))
            new_ex.append(e)
            continue
        while e['unlocked_by'] is not None and not slugs[e['unlocked_by']]['core']:
            print('{} parent is {}'.format(e['slug'], e['unlocked_by']))
            e['unlocked_by'] = slugs[e['unlocked_by']]['unlocked_by']
        new_ex.append(e)
    return new_ex

if __name__ == '__main__':
    config = load()
    lce = len(config['exercises'])
    print(f"len exercises: {lce}")
    config['exercises'] = update_exercises(config['exercises'])
    print(f"len new exercises: {len(config['exercises'])}")
    if lce == len(config['exercises']):
        save(config)
```
@coriolinus coriolinus changed the title WIP: Exercise tree Exercise tree Jun 22, 2018
@coriolinus
Copy link
Member Author

@exercism/rust This now passes all its checks, and is the last thing blocking #543, so I'd appreciate it if you guys could take a look at it.

@petertseng
Copy link
Member

As a first step to reviewing this. I know that there is a graphical visualiser for the new exercise structure. Given that every exercise is core or unlocked by core, I think I don't need the full graphical visualiser (and/or install whatever is necessary to get it running) and I just want to write a textual one. So here is one real quick:

require 'json'

r = JSON.parse(ARGV.empty? ? File.read("#{__dir__}/config.json") : ARGF.read)

cores = []
unlocks = Hash.new { |h, k| h[k] = [] }

r['exercises'].each { |x|
  if x['deprecated']
    puts "[deprecated] #{x['slug']}"
  elsif x['core']
    cores << x['slug']
  else
    unlocks[x['unlocked_by']] << x['slug']
  end
}

cores.each { |x|
  puts x
  unlocks[x].each { |u| puts "    #{u}" }
}

And the output:

[deprecated] two-fer
[deprecated] nucleotide-codons
[deprecated] hexadecimal
hello-world
    gigasecond
    leap
    raindrops
    reverse-string
    nth-prime
    bob
    beer-song
    proverb
    difference-of-squares
    sum-of-multiples
    grains
    prime-factors
    armstrong-numbers
pythagorean-triplet
    series
    collatz-conjecture
    diffie-hellman
saddle-points
    isogram
    say
    run-length-encoding
    isbn-verifier
    perfect-numbers
    hamming
    scrabble-score
    pangram
    all-your-base
    allergies
    variable-length-quantity
    pig-latin
clock
    simple-linked-list
    pascals-triangle
    nucleotide-count
    etl
    acronym
    sieve
    rna-transcription
    triangle
    grade-school
    binary-search
    robot-simulator
    queen-attack
    bowling
    tournament
    alphametics
    two-bucket
    spiral-matrix
    palindrome-products
luhn
    largest-series-product
    word-count
    accumulate
    roman-numerals
    phone-number
    diamond
atbash-cipher
    crypto-square
    rotational-cipher
    simple-cipher
bracket-push
    luhn-from
sublist
    custom-set
space-age
    luhn-trait
    wordy
macros
poker
    decimal
    book-store
    dominoes
anagram
    protein-translation
    robot-name
    ocr-numbers
    react
minesweeper
    rectangles
    circular-buffer
parallel-letter-frequency
forth

@petertseng
Copy link
Member

petertseng commented Jun 25, 2018

There is the following conflict between my responsibilities and my time:

It should be my responsibility as a track maintainer to make sure the track progression is good.

However, the time I currently have on my hands means it is difficult for me to think deeply about any proposed scheme, or engage in discourse about whether this progression or that progression be better.

At present, I am able to give an Approval that simply means "this is a reasonable track progression with no obvious errors", but warn that I truly have given little thought as to whether it is better or worse than any other. Another factor in this is that I feel I will make some mistake no matter what I may propose, and it may make more sense to simply try it out and refine it based on feedback. The obvious risk is that if there are egregious errors, the first batch of students will have a bumpier path than if I had taken the proper time to engage in discourse on improving this admitted draft.

I am currently forced to take the action of a low-effort Approval, acknowledging the risk that the contrast in time spent can be seen as disrespectful. One person puts in the work, expects others to put in at least some effort, and the other person doesn't come through. I apologise for the disrespect this action may cause.

It is interesting that Luhn is not the exercise to unlock its two variants (luhn-from and luhn-trait), but I surmise there must be a good reason for that.

Copy link
Member

@petertseng petertseng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a low-effort Approval, with the caveat in my given explanation.

@coriolinus
Copy link
Member Author

There is the following conflict between my responsibilities and my time

I agree wholeheartedly. Topics are complex, and ideally, we could all sit down and hash out a good progression over the course of a week. Instead, what we have is the progression that I could put together over the course of a few hours.

We all have external responsibilities and other claims on our time. Do not feel bad if you cannot devote more time to this than you already have.

it may make more sense to simply try it out and refine it based on feedback

That was my hope, when putting it together. The chance that I got it perfect on the first try seems low. We can always change it later.

It is interesting that Luhn is not the exercise to unlock its two variants

They are in different categories. I forget which category I assigned to Luhn itself, but luhn-from (and bracket-push) are about implementing external traits, and luhn-trait (and space-age) are about potentially implementing your own.

@coriolinus
Copy link
Member Author

My intention, unless anyone objects, is to leave this until Friday the 29th, then merge.

@coriolinus coriolinus merged commit 573c3a5 into exercism:master Jun 29, 2018
@coriolinus coriolinus deleted the exercise-tree branch June 29, 2018 22:19
@petertseng
Copy link
Member

Previous script failed to show the core: false unlocked_by: null exercises. New script that does:

require 'json'

r = JSON.parse(ARGV.empty? ? File.read("#{__dir__}/config.json") : ARGF.read)

cores = []
unlocks = Hash.new { |h, k| h[k] = [] }

r['exercises'].each { |x|
  if x['deprecated']
    puts "[deprecated] #{x['slug']}"
  elsif x['core']
    cores << x['slug']
  else
    unlocks[x['unlocked_by']] << x['slug']
  end
}

unlocks[nil].each { |x|  puts "[free] #{x}" }

cores.each { |x|
  puts x
  unlocks[x].each { |u| puts "    #{u}" }
}

Since there are no such non-deprecated exercises for the Rust track, the output is unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Should we remove the twofer exercise entirely? twofer: Exercise name conflict
3 participants