Skip to content

Latest commit

 

History

History
187 lines (139 loc) · 7.42 KB

object-literals.md

File metadata and controls

187 lines (139 loc) · 7.42 KB

Object Literals

Sometimes it's really convenient to be able to write a whole object inline. In Pony, this is called an object literal, and it does pretty much exactly what an object literal in JavaScript does: it creates an object that you can use immediately.

But Pony is statically typed, so an object literal also creates an anonymous type that the object literal fulfills. This is similar to anonymous classes in Java and C#. In Pony, an anonymous type can provide any number of traits and interfaces.

What's this look like, then?

It basically looks like any other type definition, but with some small differences. Here's a simple one:

object
  fun apply(): String => "hi"
end

Ok, that's pretty trivial. Let's extend it so that it explicitly provides an interface so that the compiler will make sure the anonymous type fulfills that interface. You can use the same notation to provide traits as well.

object is Hashable
  fun apply(): String => "hi"
  fun hash(): U64 => this().hash()
end

What we can't do is specify constructors in an object literal, because the literal is the constructor. So how do we assign to fields? Well, we just assign to them. For example:

class Foo
  fun foo(str: String): Hashable =>
    object is Hashable
      let s: String = str
      fun apply(): String => s
      fun hash(): U64 => s.hash()
    end

When we assign to a field in the constructor, we are capturing from the lexical scope the object literal is in. Pretty fun stuff! It lets us have arbitrarily complex closures that can even have multiple entry points (i.e. functions you can call on a closure).

An object literal with fields is returned as a ref by default unless an explicit reference capability is declared by specifying the capability after the object keyword. For example, an object with sendable captured references can be declared as iso if needed:

class Foo
  fun foo(str: String): Hashable iso^ =>
    object iso is Hashable
      let s: String = str
      fun apply(): String => s
      fun hash(): U64 => s.hash()
    end

We can also implicitly capture values from the lexical scope by using them in the object literal. Sometimes values that aren't local variables, aren't fields, and aren't parameters of a function are called free variables. By using them in a function, we are closing over them - that is, capturing them. The code above could be written without the field s:

class Foo
  fun foo(str: String): Hashable iso^ =>
    object iso is Hashable
      fun apply(): String => str
      fun hash(): U64 => str.hash()
    end

Lambdas

Arbitrarily complex closures are nice, but sometimes we just want a simple closure. In Pony, you can use the lambdas for that. A lambda is written as a function (implicitly named apply) enclosed in curly brackets:

{(s: String): String => "lambda: " + s }

This produces the same code as:

object
  fun apply(s: String): String => "lambda: " + s
end

The reference capability of the lambda object can be declared by appending it after the closing curly bracket:

{(s: String): String => "lambda: " + s } iso

This produces the same code as:

object iso
  fun apply(s: String): String => "lambda: " + s
end

Lambdas can be used to capture from the lexical scope in the same way as object literals can:

class Foo
  new create(env:Env) =>
    foo({(s: String) => env.out.print(s) })

  fun foo(f: {(String)}) =>
    f("Hello World")

assign from the lexical scope to a field. This is done by adding a second argument list after the parameters:

class Foo
  new create(env:Env) =>
    foo({(s: String)(env) => env.out.print(s) })

  fun foo(f: {(String)}) =>
    f("Hello World")

It's also possible to use a capture list to create new names for things. A capture list is a second parenthesised list after the parameters:

  new create(env:Env) =>
    foo({(s: String)(myenv = env) => myenv.out.print(s) })

The type of a lambda is also declared using curly brackets. Within the brackets, the function parameter types are specified within parentheses followed by an optional colon and return type. The example above uses {(String)} to be the type of a lambda function that takes a String as an argument and returns nothing.

If the lambda object is not declared with a specific reference capability, the reference capability is inferred from the structure of the lambda. If the lambda does not have any captured references, it will be val by default; if it does have captured references, it will be ref by default. The following is an example of a val lambda object:

actor Main
  new create(env:Env) =>
    let l = List[U32]
    l.push(10).push(20).push(30).push(40)
    let r = reduce(l, 0, {(a:U32, b:U32): U32 => a + b })
    env.out.print("Result: " + r.string())

  fun reduce(l: List[U32], acc: U32, f: {(U32, U32): U32} val): U32 =>
    try
      let acc' = f(acc, l.shift()?)
      reduce(l, acc', f)
    else
      acc
    end

The reduce method in this example requires the lambda type for the f parameter to require a reference capability of val. The lambda object passed in as an argument does not need to declare an explicit reference capability because val is the default for a lambda that does not capture anything.

As mentioned previously the lambda desugars to an object literal with an apply method. The reference capability for the apply method defaults to box like any other method. In a lambda that captures this needs to be ref if the function needs to modify any of the captured variables or call ref methods on them. The reference capability for the method (versus the reference capability for the object which was described above) is defined by putting the capability before the parenthesized argument list.

actor Main
  new create(env:Env) =>
    let l = List[String]
    l.push("hello").push("world")
    var count = U32(0)
    for_each(l, {ref(s:String) =>
      env.out.print(s)
      count = count + 1
    })
    // Displays '0' as the count
    env.out.print("Count: " + count.string())

  fun for_each(l: List[String], f: {ref(String)} ref) =>
    try
      f(l.shift()?)
      for_each(l, f)
    end

This example declares the type of the apply function that is generated by the lambda expression as being ref. The lambda type declaration for the f parameter in the for_each method also declares it as ref. The reference capability of the lambda type must also be ref so that the method can be called. The lambda object does not need to declare an explicit reference capability because ref is the default for a lambda that has captures.

Actor literals

Normally, an object literal is an instance of an anonymous class. To make it an instance of an anonymous actor, just include one or more behaviours in the object literal definition.

object
  be apply() => env.out.print("hi")
end

An actor literal is always returned as a tag.

Primitive literals

When an anonymous type has no fields and no behaviours (like, for example, an object literal declared as a lambda literal), the compiler generates it as an anonymous primitive, unless a non-val reference capability is explicitly given. This means no memory allocation is needed to generate an instance of that type.

In other words, in Pony, a lambda that doesn't close over anything has no memory allocation overhead. Nice.

A primitive literal is always returned as a val.