Previous ... Next

Input Output with Object Capabilities

(1/5) Controlling non determinism

Traditionally, in imperative languages I/O and side effects can happen everywhere, while in pure functional languages like Haskell they are kept in check by monads. In 42 we have type modifiers to keep mutation and aliasing under control. With only the features shown up to now, 42 is a deterministic language, thus every expression that only takes immutable objects can be called multiple times with the same values and will produce the same result.
The whole caching system relies on this property to work.
Thus input output, random numbers and any other kind of observable non determinism must preserve this property.
Introducing object capabilities:
An object capability is a mutable object whose methods can do some non deterministic, or otherwise privileged operation. If a method is given a «» reference to an object capability, then it can be non deterministic, otherwise it will be deterministic. Creation of new object capabilities is only possible by either relying on an existent object capability or by using a capability method: a method whose name starts with «»; however those specially named methods can only be called from a main, from another capability method or from a «» method of a capability object.

(2/5) Example: File System

To read and write files we need to load a «» library as shown below:

«» is the local name for the library located at «». The «» decorator embeds the library in the current environment. We can now use our file system:
The crucial point in the former code example is the call to «». This instantiates a capability object using the capability method «». We could write the code inside a method in the following way:
Note how we pass the capability object explicitly to the method. This is the most common style, and have great testing advantages: Indeed, «» corresponds to the following interface:
and «» is simply an implementation of such interface connected with the real file system. Thus, we can write a simple mock to check that the function behaves as expected:

(3/5) Object capabilities programming patterns

The advantage of the division of the file system in an interface and a «» implementation are not limited to testing. For example, the user could embed some security and some restrictions in an alternative implementation of a file system. Consider the following code:

Any code that would take in input a «» would have a limited access to the file system; only able to read and write on «» files. Here we see for the first time the decorator «». «» explores all the nested classes of the decorated code, and if there is at least a «» annotation, all the other members of such nested class will become private. Methods implemented from interfaces are left untouched. In the example above, «» leaves the two factory methods visible and hides the field getter and exposer. We discuss «» more in the detail later.
Instances of «» are capability objects; note how «» can even declare a «» method. In this way for the user there is no syntactical difference between using «» or using «». Capability objects are a useful abstraction and can be designed and implemented by normal 42 programs; they are not just a way for the language implementation to expose native code. We have just shown that new object capabilities can easy be defined by simple wrapping over existing capability objects.
Since inner is of type «», this programming patterns allows us to layer many levels of security / restrictions on top of a capability object, as shown below:

(4/5) Connection with other languages

In general, all the non determinism in 42 is obtained by communicating with other languages. 42 allows us to connect with Java, and Java allows us to connect with C/assembly. The best way to connect with java is to use the library «» as shown below:

The code above loads the library «». It is a generic library: before being used we need to provide a name for the Java slave. A 42 program is not a single process but a cluster of intercommunicating Java processes. There is one master process where the 42 computation is actually running and many other slave processes allowing safe input output and safe interaction with arbitrary code. Such slave processes have their own name: in this case «». Slaves also have a set of options, that can be specified between the «». We do not describe the details of those options here. The class «» can now be used to communicate with the Java slave as shown below:

    |      "any string computed in Java using "+id+" and "+msg);
    |    }
    |  }
    """)
  S.Opt text = j.askEvent(key=S"BarAsk", id=S"anId",msg=S"aMsg")
  {}:Test"OptOk"(actual=text, expected=S"""
    |<"any string computed in Java using anId and aMsg">
    """.trim())
  )
]]>
This code asks the event «» on the channel «». The Java code registers the capacity of answering to the channel «» and computes an answer parameterized over «» and «». The method «» is synchronous: it will wait for Java to provide an answer as an optional string; optional since Java can return «» as a result. As you can see, you can embed arbitrary Java code in 42 and communicate back and forth serializing data and instructions as strings. Synchronous communication is sometimes undesirable. For example, to use Java to open a GUI it would be better to have asynchronous communication and a queue of events. You can do this with «», as shown below:
{
    |      System.out.println("Ping Event received ping "+msg);
    |      event.submitEvent("BarOut","fromJavaToL42","pong");
    |      });
    |    event.registerEvent("Kill",(id,msg)->{
    |      System.out.println("Doing cleanup before slave JVM is killed");
    |      System.exit(0);
    |      });
    |    }
    |  }
    """)
  model=Model(count=0I, j=j)
  model.fromJavaToL42(msg=S"Initial message")
  keys=S.List[S"BarOut"]
  models=J.Handler.Map[key=S"BarOut" mutVal=model]
  for e in j(keys) ( e>>models )
  Debug(S"Completed")
  )
]]>
The class «» handles the events inside of 42: if Java send an event with id «» then the method «» will be called. In turn, such method sends to java the message «» on channel «» using «» up to 40 times, and kills the slave JVM after that. In «» we initialize the slave JVM to respond to two channels: «» and «». In our example Java will submit an asynchronous event to 42 as a response to the «» event and will terminate the slave on any «» event. The slave should always terminate its JVM when receiving a kill, but can do any kind of clean-up before that. After a JVM is terminated, it can be restarted by simply calling «» again. Finally, we set up the event loop: An event loop will collect events from a list of «» and dispatch them to a map of «», mapping every key to a specific «». Note that both «» and «» are mutable objects. In this way we can dynamically register and unregister keys/models by mutating «» and «». If the JVM is killed or the list of keys becomes empty, the event loop «» will terminate. The operation «>models]]>» dispatches the event to the model.
We need to use two different channels («» and «») to distinguish if an event is should be handled by 42 or by Java.

(5/5) Object capabilities summary

  • While most languages run in a single process, 42 runs in a cluster of processes; this is needed so that the master process is protected from any slave process going into undefined behaviour. This is the final piece of the puzzle allowing the correctness properties of 42 to be ensured in any circumstance.
  • To enable non deterministic behaviour we need to call those specially named «» methods. Since they can only be called in a few controlled places, we can control what parts of the code can perform non determinism by explicitly passing capability objects.
  • Capability objects are a very convenient centralized point of control to inject security or other kinds of restrictions.

Digressions / Expansions

Non deterministic errors

When discussing errors, we did not mention how to handle errors happening in a non deterministic way; for example, how to recover when the execution run out of memory space. In 42 this is modelled by non deterministic errors. They can only be caught in a main, in another capability method or in a «» method of a capability object. AdamsTowel offers a single non deterministic error: «». When a non deterministic error happens, we can recover it by catching an «». The code below shows how to cause a stack overflow and to recover from it.

That is, to recover from a non deterministic error we need to satisfy both the requirements of «» non determinism and of strong error safety.

Aborting wasteful cache eagers

«» methods may return a cached result; such result is guaranteed to be the same that would be computed if we were to directly execute the method. How does this work if the method is non terminating or simply outrageously slow? Those eager cache methods will eagerly spend precious machine resources. If the results of those computations are ever needed by a main, the whole 42 process will get stuck waiting, as it would indeed happen if we were to directly execute the method. All good: in this case 42 correctly lifted the behavioural bug into caching. However, if the result is never needed by a main, it would be nice to be able to stop those runaway pointless computations. We can obtain this by calling «».
This works no matter if they are in loop, simply slow, or stuck waiting on another cache being slowly computed. In some cases, we can even have multiple computations stuck waiting on each other in a circular fashion.

      Previous ... Next