Pyret is a programming language designed to serve as an outstanding choice for programming education while exploring the confluence of scripting and functional programming. It's under active design and development, and free to use or modify.

Examples Set Sail News & Discussion Code

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,
        type: "get",
        params: [list: ]
      }).status-code
  end
  get-status("http://google.com/") is 200
  get-status("http://healthcare.gov/") is 500
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.


Pyret supports both end and ; as block terminators. It requires some terminator in order to parse unambiguously without resorting to whitespace, but end can be verbose in short programs.

data BinTree: leaf | node(value, left, right);

fun square(n): n * n;

check:
  [list: 1, 2, 3].filter(_ > 1) is [list: 2, 3]
  [list: "one", "two"].map(string-toupper) is [list: "ONE", "TWO"]
  [list: [list: 1], [list: 2]].map(_.first) is [list: 1, 2]
end

Pyret embraces and provides syntactic sugar for functional patterns. The special _ syntax turns expressions into functions: _ > 1 means fun(x): x > 1 end. Pyret goes further and works this syntax into the common dot syntax for field lookup, blending functional and object-oriented styles.


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;
where:
  fun square(x): x * x;
  fun within(delta, target):
    lam(actual): num-abs(actual - target) < delta;;

  dsquare = d-dx(square)

  dsquare(5) satisfies within(0.1, 10)
  dsquare(10) satisfies within(0.1, 20)
end

point-methods = {
  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 the 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

Annotations

Most “scripting” languages don't support annotations for checking parameters and return values; Pyret does.

Python
def square(n):
  return n * n
Pyret
fun square(n :: Number) -> Number:
  n * n
end 

Optional Annotations

But Pyret doesn't force you to annotate everything, as some other languages do.

Java
static int square(int n) {
  return n * n;
} 
Pyret
fun square(n) -> Number:
  n * n
end

Refinements in Annotations

Pyret allows you to (optionally) describe refinements of data.

Python
def insert(e, s):
  # tree insertion but with
  # invariants neither
  # stated nor checked
Pyret
fun insert(e :: Number,
           s :: BST % (is-balanced))
    -> BST % (is-balanced):
  # self-balancing tree insertion
end

Numbers

Pyret has numbers, because we believe an 8GB machine should not limit students to using just 32 bits.

Java
// this is not true
((1 / 3) * 3) == 1
Pyret
# this is true
((1 / 3) * 3) == 1

Simple Testing

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.

Python
import unittest
class TestLists(unittest.TestCase):
  def test_empty_first(self):
    def lookup_first(lst):
      lst[0]
    self.assertRaises(
        IndexError,
        lookup_first,
        [])

  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()
Pyret
check:
  empty.first raises "not-found"
  [list: 1,2,3,4,5].first is 1
  [list: 2,4,6,8].first is 2
end

Structured Data

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.

Python
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
Pyret
data BinTree:
  | leaf
  | node(v, l, r)
end

Structural Data

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.

OCaml
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
    ...
Pyret
data Animal:
  | elephant(name, weight)
  | tiger(name, stripes)
  | horse(name, races-won)
  ...
end

fun animal-name(a :: Animal):
  a.name
end

Racket
(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)]
    ...))
Pyret
data Animal:
  | elephant(name, weight)
  | tiger(name, stripes)
  | horse(name, races-won)
  ...
end

fun animal-name(a :: Animal):
  a.name
end

Embracing Substitutability

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.

JavaScript
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)
Pyret
o = {
  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

Ruby
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
Pyret
o = {
  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