4 Pyret Style Guide
Ahoy matey! Here be the style guide for Pyret. Follow me rules to find the hidden treasure, or walk the plank!
4.1 General
4.1.1 Indentation
You should indent your code blocks using two spaces (not tabs).
4.1.2 Line Length
The total length of your lines should be kept under 80 characters. The Pyret editor displays a dotted blue at the 80 character mark when any of your lines are over this limit.
For overly long lines, it’s actually really hard to figure out where to put in good line breaks. This is in every language. Look for something that tries to match the logical structure of your program.
fun f(x, y, z): |
g(some-very-long-thing(x * x, y + y), other-very-long-thing((x + y + z) / (2 * 3 * 4))) |
end |
fun f(x, y, z): |
sensible-name-1 = some-very-long-thing(x * x, y + y) |
sensible-name-2 = other-very-long-thing((x + y + z) / (2 * 3 * 4)) |
g(sensible-name-1, sensible-name-2) |
end |
Not only does this shorten lines, it makes it clearer what all these pieces are doing, helping a later reader (who may be yourself!).
4.1.3 Variable Naming
The programming language world rages about the use of camelCase versus under_scores in variable names. Pyret’s syntax supports both, but we can do better.
In Pyret, you can use dashes (-) inside variable names.This is sometimes called “kebab case”, but it would be more accurate to call it “shish case”. Thus, you would write camel-case and under-scores. Unlike underscores, dashes don’t need a shift key (or disappear when text is underlined by an environment). Unlike camelcase, dashes don’t create ambiguities (what if one of the words is in all-caps?). Dashes are also humanizing: they make your program look that little bit more like human prose.
Most languages can’t support dashes because the dash also stands for infix subtraction. In Pyret, subtraction must be surrounded by space. Therefore, camel-case is a name whereas camel - case is subtraction.
4.1.3.1 Naming Constants
MY-COUNT = 100 |
e = 2.7182 |
4.1.3.2 Reusing Variable Names
fun f(x): x + 1 end |
fun g(x): x + 2 end |
fun h(x): |
x = 4 |
x + 1 |
end |
4.1.4 File Naming
Use .arr as the extension for Pyret files.
4.1.5 Example and Tests
We use the syntax of testing to represent two different tasks: examples, which help us explore a problem and take steps towards deriving a solution, and tests, which are designed to find errors. These are subtly different.
examples: |
f(10) is 25 |
f(20) is-not 2000 |
end |
4.2 Functions
4.2.1 Naming
Give functions descriptive names. Do the same for arguments. That way, a quick scan of a function’s header will tell you what it does and what the arguments are supposed to do.
4.2.2 Documentation Strings
fun insert(x, l): |
doc: "consumes sorted list l; returns it with x in the right place" |
... |
end |
``` |
fun f(x): |
doc: ```This is a |
multi-line |
comment here.``` |
x + x |
end |
4.2.3 Annotations
fun str-len(str :: String) -> Number: |
# ... |
end |
fun sum-str-lengths(lst :: List<String>) -> Number: |
# ... |
end |
fun non-negative(n :: Number) -> Boolean: |
n >= 0 |
end |
fun sqrt(n :: Number%(non-negative)) -> Number: |
# ... |
end |
4.2.4 Parametric Annotations
You should avoid using Any as the subtype of a parametric data structure, like a List.
In general, a List<Any> definition is often undesirable because it indicates elements of any type can be members of the list. In particular, this subtly implies that multiple elements of different types could exist. In other words, [list: 1, "apple", true] would satisfy the annotation List<Any>. Creating Lists like this is usually bad practice.
In Pyret, we can do this by parametrizing the type. Rather than
fun search(lst :: List<Any>) -> Any: ... end |
we’d now do something like
fun search<A>(lst :: List<A>) -> A: ... end |
Now, the elements of lst can be of any type, but all elements of lst will be of type A (along with the return value of search)! Note that the actual name of the type parameter is arbitrary (we could have written search<SomeOtherName> instead of search<A>), but it is convention to use a single letter for parametric annotations.
4.2.5 Testing
You should test every function you write for both general cases and edge cases.
fun double(n :: Number) -> Number: |
n * 2 |
where: |
double(0) is 0 |
double(5) is 10 |
double(-5) is -10 |
double(100) is 200 |
double(-100) is -200 |
end |
4.3 Data
4.3.1 Definitions
Wherever possible, provide annotations in Data definitions:
data Animal: |
| snake(name :: String) |
| dillo(weight :: Number, living :: Boolean) |
end |
4.3.2 Cases
cases (Animal) a: |
| snake(s) => s == "Dewey" |
| dillo(w, l) => (w < 10) and l |
end |
cases (Animal) a: |
| snake(s) => ... |
| dillo(w, _) => ... |
end |
cases (Animal) a: |
| snake(s) => ... |
| dillo(dummy, dummy) => ... |
end |
cases (Animal) a: |
| snake(s) => ... |
| dillo(_, _) => ... |
end |
cases (Animal) a: |
| snake(s) => ... |
| dillo(_, _) => raise("Serpents only, please!") |
end |
4.4 Boolean Expressions
4.4.1 Ask Blocks
If you have an if block containing a lot of conditionals (greater than 3 branches), you should convert it to an ask block. This aligns your conditionals along the left side of the block, which makes your code look cleaner and more readable.
For example, this if block is very verbose:
fun is-this-noah(str :: String) -> String: |
if str == "Noah": |
"Yes" |
else if str == "Nosh": |
"Close" |
else if str == "No": |
"ah" |
else if str == "Noa": |
"I don’t think so" |
else: |
"No" |
end |
end |
However, we can make it more concise using an ask block:
fun is-this-noah(str :: String) -> String: |
Ask: |
| str == "Noah" then: "Yes" |
| str == "Nosh" then: "Close" |
| str == "No" then: "ah" |
| str == "Noa" then: "I don’t think so" |
| otherwise: "No" |
end |
end |
4.4.2 Concise Conditionals
You should never determine the boolean value of something by doing:
if some-boolean == true: |
... |
else: |
... |
end |
Instead, you should use:
if some-boolean: |
... |
else: |
... |
end |
Similarly, do not use an if expression to evalulate a predicate, and then return true or false. For example, this would be considered incorrect:
if my-expression: |
true |
else: |
false |
end |
Instead, the entire if expression above can be replaced with my-expression.
4.5 Naming Intermediate Expressions
4.5.1 Local Variables
fun hypo-len(a, b): |
num-sqrt((a * a) + (b * b)) |
end |
fun hypo-len(a, b): |
a2 = a * a |
b2 = b * b |
sum-of-other-two-sides = a2 + b2 |
num-sqrt(sum-of-other-two-sides) |
end |
4.5.2 Beware of var!
fun hypo-len(a, b): |
var a2 = a * a |
var b2 = b * b |
var sum-of-other-two-sides = a2 + b2 |
num-sqrt(sum-of-other-two-sides) |
end |