Programming in Pyret
fun to-celsius(f): (f - 32) * (5 / 9) end for each(str from [list: "Ahoy", "world!"]): print(str) end
Pyret has Python-inspired syntax for functions, lists, and operators. Iteration constructs are designed to be evocative of those in other languages.
Pyret makes testing a natural part of the programming process. Functions can
end in a where:
clause that holds unit tests for the
function. These assertions are checked dynamically.
fun sum(l): cases (List) l: | empty => 0 | link(first, rest) => first + sum(rest) end where: sum([list: ]) is 0 sum([list: 1, 2, 3]) is 6 end
data BinTree: | leaf | node(value, left :: BinTree, right :: BinTree) end
Pyret allows for concise, expressive, recursive data declarations. Type annotations are optional and can be added incrementally, to serve a variety of pedagogic styles and curricular needs.
In addition to where:
blocks, which are attached to
individual definitions (and hence usually contain unit tests), you
can also write check:
blocks at the top level, for
general program testing. Both are scope delimiters. Therefore, you
can use them to write local definitions that are useful for testing
but not relevant to the program at large.
check: fun get-status(url): request({ url: url, verb: "get", params: [list: ] }).status-code end get-status("http://google.com/") is 200 end
Some thoughts on syntax
We believe indentation is critical for readable code, but we don't want the whitespace of the program to determine its meaning. Rather, the meaning of the program should determine its indentation structure. Indentation becomes just another context-sensitive rule.
Unambiguous syntax (the reason for explicit end
delimiters)
means you can copy-and-paste code from email or the Web, and its meaning won't
change. Your IDE can help you reindent code without worrying that doing so will
change the meaning of the program.
We haven't yet decided on the indentation rules because we want to see the language in use for a while before we codify these.
Real tests need to accomodate more than equality tests. Pyret supports these
generally with the satisfies
form, which can be used to check
satisfaction of an arbitrary predicate.
eps = 0.001 fun d-dx(f): doc: "Approximate the derivative of f" lam(x): (f(x + eps) - f(x)) / eps end where: fun square(x): x * x end fun around(delta, target): lam(actual): num-abs(actual - target) < delta end end dsquare = d-dx(square) dsquare(5) satisfies around(0.1, 10) dsquare(10) satisfies around(0.1, 20) end
point-methods = { method dist(self, other): ysquared = num-expt(other.y - self.y, 2) xsquared = num-expt(other.x - self.x, 2) num-sqrt(ysquared + xsquared) end } fun make-point(x, y): point-methods.{ x: x, y: y } end check: p1 = make-point(1, 2) p2 = make-point(1, 5) p1.dist(p2) is 3 end
Pyret has a straightforward object model, from which more complex
patterns can be defined. An object is defined by methods and fields within
curly braces (as in point-methods
), and can be extended
with .{}
. This example shows a simple class-like
pattern built up from simple objects. Objects, like most other values in
Pyret, are immutable by default, so instances of points are created by
extending an object containing point methods.
Like what you see? Sign up for the announcements mailing list and get notified when Pyret has a stable release. Or, if you want to try things out in their early state, just get started!
Highlights vs. Existing Languages
Most “scripting” languages don't support checking annotations on parameters out of the box, Pyret does.
def square(n : int) -> int: return n * n square("5") # Error at multiplication: # Can't multiply sequence by # non-int of type 'str'
fun square(n :: Number) -> Number: n * n end square("5") # With type checker off: # The Number annotation was not # satisfied by the value "5" # With type checker on: # Number is incompatible with String
But Pyret doesn't force you to annotate everything, as some other languages do.
static int square(int n) { return n * n; }
fun square(n) -> Number: n * n end
Pyret allows you to (optionally) describe refinements of data.
def insert(e, s): # tree insertion but with # invariants neither # stated nor checked
fun insert(e :: Number, s :: BST % (is-balanced)) -> BST % (is-balanced): # self-balancing tree insertion end
Pyret has numbers, because we believe an 8GB machine should not limit students to using just 32 bits.
// this is not true ((1 / 3) * 3) == 1
# this is true ((1 / 3) * 3) == 1
Friction in the testing process makes it hard to work even simple unit tests into early programming. Pyret removes boilerplate to put testing in its rightful place in the programming process.
import unittest class TestLists(unittest.TestCase): def test_empty_first(self): self.assertRaises(IndexError, lambda: [][0]) def test_1to5(self): self.assertEqual([1,2,3,4,5][0], 1) def test_evens(self): self.assertEqual([2,4,6,8][0], 2) if __name__ == '__main__': unittest.main()
check: empty.first raises "not-found" [list: 1,2,3,4,5].first is 1 [list: 2,4,6,8].first is 2 end
Being able to describe data well is central to designing and
structuring programs. Pyret offers elegant mechanisms for writing data
definitions without the cognitive or syntactic overhead of classes. We
believe the only reason __init__
will not become this
generation's public static void
is that Python textbooks
have begun to shun structured data, returning us to the 1970s when
everything was squeezed into a single-dimensional data structure.
class BinTree: pass class leaf(BinTree): def __init__(self): pass class node(BinTree): def __init__(self, v, l, r): self.v = v self.l = l self.r = r
data BinTree: | leaf | node(v, l, r) end
Pyret is flexible in the use of structured data, and exposes a simple object pattern underlying it to allow for structural code alongside more nominal patterns.
type animal = | Elephant of string * float | Tiger of string * float | Horse of string * int ... let name_of_animal a = match a with | Elephant(name, _) | Tiger(name, _) | Horse(name, _) -> name ...
data Animal: | elephant(name, weight) | tiger(name, stripes) | horse(name, races-won) ... end fun animal-name(a :: Animal): a.name end
(struct elephant (name weight)) (struct tiger (name stripes)) (struct horse (name races-won)) ... (define (animal-name a) (cond [(elephant? a) (elephant-name a)] [(tiger? a) (tiger-name a)] [(horse? a) (horse-name a)] ...))
data Animal: | elephant(name, weight) | tiger(name, stripes) | horse(name, races-won) ... end fun animal-name(a :: Animal): a.name end
A design goal of Pyret's syntax and semantics is to embrace the substitutability of equivalent expressions as much as possible. This is in contrast to, for example, some scripting languages, in which what looks like binding an expression to a temporary name changes program behavior.
var o = { my_method: function(x) { return this.y + x; }, y: 10 } o.my_method(5) === 15 // true method_as_fun = o.my_method method_as_fun(5) // either error or NaN // (depending on strict mode)
o = { method my-method(self, x): self.y + x end, y: 10 } method-as-fun = o.my-method check: o.my-method(5) is 15 method-as-fun(5) is 15 end
o = Object.new def o.my_method(x) self.y + x end def o.y 10 end o.my_method(5) == 15 # true method_as_fun = o.my_method # Wrong number of arguments, 0 for 1
o = { method my-method(self, x): self.y + x end, y: 10 } method-as-fun = o.my-method check: o.my-method(5) is 15 method-as-fun(5) is 15 end