Programs that create images give immediate visual feedback.
Functions that operate on images enable students to learn about function composition in an medium that is both engaging and educational:
composing images rather than numbers provides an immediate visual artifact, which makes it easier to spot errors and match a result to an intended design.
Multiple curricula take advantage of these features. In Bootstrap, lessons like
Make a Flag
and
Hour of Code
are centered around it. Likewise,
DCIC also uses them for a quick and attractive introduction to programming.
Pyret's
number system
supports exact rational arithmetic for many operations. This avoids having
floating point as a curricular dependency early on. When approximations become
inevitable, they have an explicit representation:
Roughnums.
Pyret provides a pleasant syntax for writing illustrative examples of
how functions should behave. These serve not only as documentation,
but are also useful for students to demonstrate their understanding of
a problem before they start programming. The
Examplar
project is built entirely around this feature.
Examples can be written
as part of the function definition or just after or
before. Stand-alone example blocks optionally take a descriptive
string, which not only improve documentation but are also helpful
for understanding failures.
Pyret incorporates
Tables
as a basic, built-in datatype.
We have argued
that tables are perhaps the key datatype for early programming,
and DCIC gives them pride of place,
so having high-quality programming support for tables is critical.
Pyret has
algebraic data types
and
case matching
in the style of most modern
languages. Direct construction, along with built-in testing, make it
straightforward to incrementally build up data structure definitions.
Pyret allows you to optionally write annotations on any variable binding.
These are checked dynamically.
Dynamic checking can create a lot of run-time overhead (even changing a program's
big-O characteristics). Pyret uses a pleasant trade-off: it checks the
outermost annotation only. This takes constant time and catches most errors,
but lets a few subtler ones to be checked later.
Pyret also ships with an experimental type checker, which covers most
but not all of the language's features (tables are particularly tricky).
You can access the type-checker by clicking on the Run button's drop-down
to select the type-checker. Observe how the default program, even with the
call to s2cp
commented out, results in an error!
Note: after trying out the type-checker, please revert to the regular Run button.
Otherwise, you may get odd errors in some of the other feature tabs, which were
intentionally not designed to pass the type-checker.
Pyret provides an extensive set of features for
writing tests.
You
don't need a separate library or “build file” or write or run them.
It's part of the programming language, because we believe it is
imporant to write robust programs, and testing is a major component of
that.
In many languages, the only easy way to examine values during program execution is
by using print commands, leading to what is often called “printf-debugging”.
Pyret recognizes the value of examining dynamic values and provides an elegant
way of doing this:
spies.
Unlike with a “printf”, spying presents well-formatted output: you can trace back
to the source, the output looks different to distinguish from other printed output,
values in the display are clickable, etc.
Pyret has full, proper, lexically-scoped higher-order functions
(colloquially referred to as “lambdas”).
Because they are used in many contexts, the language has three different syntaxes for it.
Pick the one you find most convenient!
Pyret has
“for loops”.
They are more useful and powerful
than those in most other programming languages.
Though many libraries in Pyret use lists, Pyret also has built-in support for sets, which are more appropriate in some
settings than lists. In Pyret, sets behave as one would truly want them to: there is no such thing as the “first” element (as with lists),
and instead programs use pick
to extract an element.
What happens if you pick
multiple times? Pyret does not want programs to get dependent on a particular behavior—sets,
after all, do not have an order, but if the implementation acted as if they did, programs would grow to depend on that.
Therefore, a
Pick
is like a “first and rest”, except
there's no guarantee which “first” element you will get; the “rest” corresponds to all the other elements
depending on what was chosen “first”.
Unlike in many other languages, you may have noticed that […]
is
not reserved for lists; instead, programs need to write [list: …]
to construct a list. This is because Pyret does not want to over-privilege
a single datatype; different programs depend on different collections of them,
and it is nice to be able to define them comfortably.
Thus, in Pyret, several datatypes have convenient constructors besides lists:
sets and dictionaries are good examples. But programs can also define
their own constructors! The process is a bit unwieldy so that custom
constructors also have good performance. But this unwieldy code only needs
to be written once, by the constructor creator; users of the constructor
do not see this.
Pyret has support for tuples, which are collections of ordered data of fixed size. Elements of tuples are accessed by position.
Tuples can also be destructured at variable binding.
However, we caution against using tuples too much. We feel that tuples are best used either within a function itself, or when a
called function needs to return multiple distinct values that will immediately be destructured and named. If a tuple
flows intact through a program, after a while it becomes very difficult to tell what it represents, and it may even be
confused with other tuples that have at least as many elements.
In the latter cases, it's better to use an object with named fields, or (if the datum is sufficiently significant and
visible in many parts of the program) a fresh datatype.
Pyret has both mutable variables and mutable structures. Pyret
values are immutable by default (which reduces surprise and makes
testing much easier), but mutation is available when needed:
students just need to invoke it explicitly.
Pyret support for mutable structures is especially smart.
Mutable fields are displayed with black-and-yellow striping
(what some students have labeled the “bumblebee”). This indicates
that the value you are seeing may change. Furthermore, clicking on
the bumblebee updates it to its current value!
Modules are useful for providing new services (such as functions and types). However, sometimes users—especially educators—will
want to limit the set of visible services. For instance, certain functions may make a homework trivial, so they may
want to remove those functions from the set of visible services.
For this, Pyret provides contexts.
A context definition is a description of the entire namespace. It starts with an empty
namespace and adds to it until it has the desired contents, which it then exports.
Thus a context determines not only what names are bound and not bound, but also what they are bound to.
Pyret has a very sophisticated story when it comes to equality.
You can read much more about it
on this page.
Programmers often need to define partial functions. Rather than return “sentinel” values
(which might fail only much later in the computation or, worse, be interpreted as valid
response and never fail at all), Pyret offers two standard mechanisms.
One is to raise an exception, while the other is use the Option
type.
Exceptions make more sense when dealing with truly unexpected situations and/or
ones from which there is no meaningful path to recovery.
Options make more sense when the situation is reasonable to expect and/or is there
some meaningful value that can be provided.
Both can be used with testing.
In several curricula, including
Boostrap Data Science,
students work with programs that load and analyze raw data.
Data can be easily loaded from CSV files at public
URLs, or from
Google Sheets.
As programs get more sophisticated, it is often not enough to write
unit tests; we need to test properties. Pyret gives you
primitives
for expressing the idea of values satisfying predicates.
Pyret does not, however, provide data generators for property-based
testing, because writing those is usually instructive and also a
good way to learn about and explore biases in data.
Pyret has objects and methods. Students can therefore transition from
functions (which have a simpler calling semantics) to objects (which
are more complex, but have conveniences), and eventually also define
their own methods.