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

circular-buffer: Add canonical data #488

Merged
merged 2 commits into from
Jan 15, 2017
Merged

circular-buffer: Add canonical data #488

merged 2 commits into from
Jan 15, 2017

Conversation

petertseng
Copy link
Member

@petertseng petertseng commented Jan 8, 2017

The circular-buffer exercise was first added in 2014 May at #9, with
Ruby and Javascript as the initial implementing tracks:

Implementing tracks:

All tracks pretty much implement the same tests, except:

  • Tracks typically have the "read item just written" and "each item can
    only be read once" tests combined. These tests split the two.
  • In dynamically-typed languages, buffers may be able to store
    heterogeneous types. This concept is not expressed in these tests.
  • In some statically-typed languages, buffers use generics, such that
    buffers may store any one type of the client's choosing. This concept
    is also not expressed in these tests.
  • The final test (ensuring that overwrite drops the right items) was more
    complex: capacity 5, 3 writes, 2 reads, write, read, 4 writes, 2
    overwrites, 5 reads. It's been simplified to capacity 3, 3 writes, 1
    read, write, overwrite, 3 reads.

Closes exercism/todo#79

"Based on the operation, other keys may be present.",
"read: Reading from the buffer should produce the item located at `expected`",
"write: Writing the item located at `item` should succeed",
"readShouldRail: Reading from the buffer should fail.",
Copy link
Contributor

Choose a reason for hiding this comment

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

s/Rail/Fail/

Copy link
Member Author

@petertseng petertseng Jan 8, 2017

Choose a reason for hiding this comment

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

Excuse me! I obviously meant "complain or protest strongly and persistently about", meaning the code should rail at its user on an attempt to perform a read.

(Fixed)

"expected": 1
},
{
"operation": "readShouldFail"
Copy link
Member Author

Choose a reason for hiding this comment

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

not related to reading an item just written. A separate test, "Each item can only be read once"?

Copy link
Member Author

Choose a reason for hiding this comment

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

(our comments remain, but this comment has been addressed, as this line now goes under "each item may only be read once")

"expected": 2
},
{
"operation": "readShouldFail"
Copy link
Member Author

Choose a reason for hiding this comment

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

Not related to reading items in the order they are written.

"expected": 1
},
{
"operation": "readShouldFail"
Copy link
Contributor

Choose a reason for hiding this comment

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

This is part of a different test: "cannot read past the end of buffer" or something

"expected": 2
},
{
"operation": "readShouldFail"
Copy link
Contributor

Choose a reason for hiding this comment

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

redundant operation.

@Insti
Copy link
Contributor

Insti commented Jan 8, 2017

I think what you're doing here is good. 👍

]
},
{
"description": "clear fills up capacity for another write",
Copy link
Member Author

Choose a reason for hiding this comment

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

s/fills/frees/

@petertseng
Copy link
Member Author

All these commits are going to be squashed and I'll amend the commit message and PR description to be reasonable.

@petertseng petertseng changed the title WIP: circular-buffer: Add canonical data circular-buffer: Add canonical data Jan 8, 2017
@petertseng
Copy link
Member Author

New tests added:

  • "items cleared out of buffer can't be read"
  • "overwrite doesn't remove an already-read item" - As first commit explains, a simplified version of a test mixing reads, writes, overwrites, but the main point as I see it was to make sure that overwrite deletes the right item if a read has previosly been performend

"clear: Clear the buffer.",
"",
"Finally, note that all values are integers.",
"If your language contains generics, you may consider allowing buffers to contain other types.",
Copy link
Member Author

Choose a reason for hiding this comment

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

Note the addition of this, which should look suspiciously similar to https://github.com/exercism/x-common/blob/master/exercises/react/canonical-data.json#L38-L40

Is this valuable to point out? Well, some tracks already have it, and I didn't want this JSON file to imply that they should remove those tests or make their test suites discourage generics. In the latter case, tracks may even get questions.

"capacity": 1,
"operations": [
{
"operation": "readShouldFail"
Copy link
Contributor

@Insti Insti Jan 8, 2017

Choose a reason for hiding this comment

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

I wonder if this should be:

"operation": "read"
"expected": false

Ensuring that there is an expected result for each of the 4 methods.

Another example for writeShouldFail

"operation": "write"
"item": 1
"expected": false

Or is this being too proscriptive of implementation details?

Copy link
Member Author

@petertseng petertseng Jan 8, 2017

Choose a reason for hiding this comment

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

Okay, I think I understand.

A few things give me pause about the "expected": false to signal failure on read:

  • Not a concern: now we can't have a buffer of booleans and write false into it and expect to read a false out! (not relevant for this JSON file since it only deals with integers)
  • The explanation of read gets a little gnarly. It becomes "read: Reading from the buffer should produce the item located at expected, unless expected is false, in which case the read should fail."
  • Statically-typed languages reading this JSON file may have more trouble dealing with the fact that the value at expected might be either a boolean or an integer.

Okay, what if it's something like this: for a successful read of 1:

{
  "operation": "read",
  "item": 1,
  "expected": true,
}

and for a failing read:

{
  "operation": "read",
  "expected": false,
}

this way, expected is a boolean for all cases.

I'm not convinced I'm happy with the name expected in this usage, since now it doesn't make clear that the main thing we want to check is the value read, rather than whether it was successful. What if expected got renamed to success or shouldSucceed?

I'm not sure if this is also part of the suggestion, but I would feel odd about adding an expected: key to overwrite and clear, since having it there implies that it can be false, and I don't think we'll ever have a test that expects either of these two to fail (well, maybe overwrite on a size-0 buffer, but I think I'd rather argue that size-0 buffers should just not get created)

I am having a hard time with the "expected": false for reads, and I'm not sure that moving the item to a separate key makes things better. Any way to make this work?


This has a little to do with #401, what's the best way to represent an error here. I am having a sufficiently hard time with fitting "expected": true/false into all operations that I think I should keep them separate. This is the suggestion of #401 (comment) - errors are simply encoded differently. I know I didn't use that in any other JSON file I've written, but I managed to make null work in all of those, whereas I'm unsure whether integer/null for reads versus true/false for writes gets too confusing. Even integer/false for reads seems a bit odd.

That, and I may be a bit biased by how the go tests were written - they pretty much use those exact six operations already.

Copy link
Contributor

Choose a reason for hiding this comment

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

Command Query Responsibility Segregation.

write is a command.
overwrite is a command.
clear is a command.
read is a query.

detecting failure is a query.

It seems we are missing the failure_detected* query that indicates this.

  "cases": [
    {
      "description": "reading empty buffer should fail",
      "capacity": 1,
      "operations": [
        {
          "operation": "read",
          "expected": null
        },
        {
          "operation": "failure_detected",
          "expected": true
        }
      ]
    },

Individual tracks are still free to implement the failure_detected check however they like, either with a method or a try/catch block or whichever other system their language encourages.

* There's probably a better name for this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Even though tracks are free to implement failure_detected how they like, I am guessing that failure_detected as a separate query will tend to suggest that it be a separate function. After all, someone porting this exercise to their track might simply think "one function for each operation, right?!", because what reason do they have to think that this failure_detected operation is any different than the others (read, write, overwrite, clear)*?

This hypothetical API with a separate failure_detected operation is one I do not wish to encourage porters to implement, especially since students will subsequently see it, and be encouraged to replicate it. I tend to think it's better to make failures clear right when they happen. After all, both queries and commands have the potential of failing, so it seems cumbersome to have to check failure in a separate query. A separate failure_detected query also seems difficult to make work in potentially concurrent contexts, but luckily that is not a concern for this particular exercise!

I have a difficult time fitting in a circular buffer with command-query separation, even without failure_detected. I see from https://en.wikipedia.org/wiki/Command%E2%80%93query_separation that command-query separation has the formal definition of "methods should return a value only if they are referentially transparent and hence possess no side effects."

The read operation has a side-effect, since it removes the just-read item from the buffer!
If we wish to follow command-query separation, read is not allowed to both return the oldest value and also remove it.
Instead, we would be forced to separate the read operation into peek and dropOldestItem.
Well, hey, I might not be opposed to that API change, but I unfortunately don't think I will try to convince others that it is the right thing to do for their tracks.

*: Hoewver, I have just realized after writing this argument against failure_detected that the current JSON schema also has two "special' operations: readShouldFail and writeShouldFail. What's to tell a porter that those two operations are in any way special? ARGH! So is my proposed JSON schema flawed as well?!

Copy link
Contributor

Choose a reason for hiding this comment

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

The read operation has a side-effect, since it removes the just-read item from the buffer!

This is a good point.

Copy link
Member Author

Choose a reason for hiding this comment

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

(I have changes planned to remove readShouldFail and writeShouldFail since in arguing against failure_detected I managed to argue against the forme rtwo as well)

The circular-buffer exercise was first added in 2014 May at #9, with
Ruby and Javascript as the initial implementing tracks:

* exercism/DEPRECATED.javascript#17
* exercism/ruby#17

Implementing tracks:

* https://github.com/exercism/xcsharp/blob/master/exercises/circular-buffer/CircularBufferTest.cs
* https://github.com/exercism/xdlang/blob/master/exercises/circular-buffer/circular_buffer.d
* https://github.com/exercism/xecmascript/blob/master/exercises/circular-buffer/circular-buffer.spec.js
* https://github.com/exercism/xerlang/blob/master/exercises/circular-buffer/circular_buffer_tests.erl
* https://github.com/exercism/xfsharp/blob/master/exercises/circular-buffer/CircularBufferTest.fs
* https://github.com/exercism/xgo/blob/master/exercises/circular-buffer/circular_buffer_test.go
* https://github.com/exercism/xjavascript/blob/master/exercises/circular-buffer/circular-buffer.spec.js
* https://github.com/exercism/xlfe/blob/master/exercises/circular-buffer/test/circular-buffer-tests.lfe
* https://github.com/exercism/xlua/blob/master/exercises/circular-buffer/circular-buffer_spec.lua
* https://github.com/exercism/xpascal/blob/master/exercises/circular-buffer/uCircularBufferTests.pas
* https://github.com/exercism/xpython/blob/master/exercises/circular-buffer/circular_buffer_test.py
* https://github.com/exercism/xruby/blob/master/exercises/circular-buffer/circular_buffer_test.rb
* https://github.com/exercism/xrust/blob/master/exercises/circular-buffer/tests/circular-buffer.rs

All tracks pretty much implement the same tests, except:

* Tracks typically have the "read item just written" and "each item can
  only be read once" tests combined. These tests split the two.
* In dynamically-typed languages, buffers may be able to store
  heterogeneous types. This concept is not expressed in these tests.
* In some statically-typed languages, buffers use generics, such that
  buffers may store any one type of the client's choosing. This concept
  is also not expressed in these tests.
* The final test (ensuring that overwrite drops the right items) was more
  complex: capacity 5, 3 writes, 2 reads, write, read, 4 writes, 2
  overwrites, 5 reads. It's been simplified to capacity 3, 3 writes, 1
  read, write, overwrite, 3 reads.

Closes https://github.com/exercism/todo/issues/79
"read: Reading from the buffer should succeed if and only if `should_succeed` is true.",
" If it should succeed, it should produce the item at `expected`. ",
" If it should fail, `expected` will not be present. ",
"write: Writing the item located at `item` should succeed if and only if `should_succeed` is true.",
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to say anything like 'failure may be indicated by raising an exception or similar language dependent runtime error'

Copy link
Member Author

@petertseng petertseng Jan 14, 2017

Choose a reason for hiding this comment

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

I added it just to be sure. I have a nagging feeling that if similar sentences appear in too many places...

then we might extract them all out to the contributing guide's section on porting an exercise. But I don't know what the right answer to that is yet, so for now I'll add the sentence.

(or maybe we are worried that a porter will not read that particular section of the contributing guide? I don't know)

Copy link
Contributor

@Insti Insti Jan 14, 2017

Choose a reason for hiding this comment

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

anecdata: When I'm porting, I tend to just read the canonical-data and the readme.
I don't re-read the contributing guide often.
(I'm probably non-typical enough for this to be irrelevant information.)

This makes it clear that there are only four operations, instead of
implying that `readShouldFail` and `writeShouldFail` are potentially
separate operations.

I convinced myself to do this since I argued against a
`failure_detected` as a separate operation, but then realized that
`readShouldFail` and `writeShouldFail` suffer from the same problems.
@petertseng petertseng merged commit 3c67665 into exercism:master Jan 15, 2017
@petertseng petertseng deleted the circular-buffer-json branch January 15, 2017 00:08
petertseng added a commit to exercism/rust that referenced this pull request Nov 18, 2018
The tests are the same as the initial unversioned version of canonical-data:
exercism/problem-specifications#488

This is because all subsequent changes did not change test content:

1.0.0 (formatting change only):
exercism/problem-specifications#681
1.0.1 (clarify overwrite):
exercism/problem-specifications#892
1.1.0 (move inputs to `input` object):
exercism/problem-specifications#1186

The Rust track already had most of the tests; these mostly add some
clarity around `clear` and addds a test for an `overwrite` following a
`read`.
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.

2 participants