Ports are an abstraction for modeling variables whose values evolve
over time without the need to resort to mutable variable, such as
IORefs. More precisely, a port represents all values that a
time-dependent variable successively takes as a stream, where each
element of the stream corresponds to a state change - we can also say
that a port represents a time series.
Ports containing values of type
a are represented as
data Port a
One or more streams can be associated with a port, which contain (a subset) of the values that the port assumes while evolving over time. The values in the output stream are determined by the messages or events that are directed to the port.
Port filters serve as guards that control the values that a port can take, i.e., they screen the incoming messages and may adjust incoming port values (e.g., to ensure that all values appearing in the output stream of a port stay within certain bounds) or even reject them.
type PortFilter a = a -> a -> Maybe a
The first argument given to a port filter is the value of the port at the
moment where a given incoming messages is processed and the second argument is
that incoming value. The port filter can decide to entirely reject the value
Nothing, or it can return the value that should be assumed
by the port next. This can, but need not be the same as the incoming value.
A port without a port filter (i.e., one that accepts any incoming message) is created by
newPort :: a -> IO (Port a)
The argument constitutes the initial value of the port, but it never appears in any output stream of the port. This value is mainly important for filtering ports, which are created with
newFilteringPort :: a -> PortFilter a -> IO (Port a)
These ports apply the given port filter to any value before placing it into the output stream.
Ports can be closed, which means that all corresponding streams are ended.
closePort :: Port a -> IO ()
It can be checked whether a port is closed or not.
isClosedPort :: Port a -> IO Bool
The following function obtains a stream containing the values assumed by the port as it evolves over time.
listenToPort :: Port a -> IO [a]
Note that while this stream has to be obtained within the
IO monad, the
stream itself can be processed purely functional (i.e., an observer of the
port need not be coded in the monad). A stream contains the values assumed by
the port after
listenToPort is executed; previous values are not
contained in the stream.
The following operation sends the second argument to the port given in the first argument:
(<--) :: Port a -> a -> IO ()
As it is sometimes convenient to send a list of values to a port, the following function, which is usually more efficient than the previous one, is also available:
(<==) :: Port a -> [a] -> IO ()
More generally, we can send a function that transforms the current port value to a port:
(<-$) :: Port a -> (a -> a) -> IO ()
p <-$ (+1) would increment the current value of the port
p by one. Obviously,
p <-$ const x corresponds to
Finally, the current value of a port can be obtained within the
peekIntoPort :: Port a -> IO (Maybe a)
This function should only be used for purposes like initialisation. It breaks
the idea of observing the time series as a whole instead of individual
elements, i.e., snapshots of the port and when multiple threads are sending
messages to a port, the order of a
peekIntoPort call with respect to the
messages is usually not deterministic. If the port is closed,
Nothing is returned.
Two ports can be to be synchronised, or linked, so that they share all incoming events.
(<->) :: Port a -> Port a -> IO ()
In fact, they can even have different domains and use translation functions to map values between their domains.
linkPorts :: (a -> b) -> (b -> a) -> Port a -> Port b -> IO ()
Ports that represent an external state as a port are called proxy ports. To keep a proxy port in sync with some external state, we use Port notifiers, which either the port of a change in the external state or vice versa. The argument to a notifier is the new value. Depending on the kind of the notifier, it puts this value into the port or updates the external state.
type PortNotifier a = a -> IO ()
When a port is created, it is given the notifier that it should use to notify the external state of changes in the port. As a result it does not only produce the port, but also a second notifier that is to be invoked whenever the external state changes.
newProxyPort :: a -> PortNotifier a -> IO (Port a, PortNotifier a)
The produced notifier will take care that no feedback loop is introduced.