Skip to content

The client/supplier dichotomy

EM revolves around the concept of a concrete module, a programmatic construct that plays a seminal role within the language comparable to the position held by a class within C++ or Java. Not unlike classes, each EM module defines a programmatic boundary between its clients – users of the module – and the supplier of the module itself.

To minimize direct coupling between clients and suppliers – and therefore to increase software re-use – EM also supports module abstraction through an interface construct which clients in turn can leverage through a module proxy.

Source-code structure

Reflecting a basic dichotomy between module clients and module suppliers, consider the overall organization of a sample EM module named Mod1, whose source code will reside in a file named Mod1.em:

bob.pkg/Mod1.em
package bob.pkg

module Mod1         # client-visible feature declarations
    const C: ...
    type T:  ...
    function f( ... )
    # etc

private:            # supplier-proprietary feature declarations
    var x: ...
    function g( ... )
    # etc
end

def f(...)          # supplier-proprietary function definitions
    # body of 'f'
end

def g(...)
    # body of 'g'
end

# etc...

Starting from the top, each EM module lives within the logical scope of a package which physically corresponds to a file-system directory of the same name. As in Java or Python, an EM package will generally bear a globally-unique qualified name that identifies its supplier – bob.pkg, bob.pkg.test, dave.pkg, and so forth. By extension, all EM modules have a (globally-unique) fully-qualified canonical namebob.pkg/Mod1 in the current example – though in practice you'll invariably refer to modules using simple names like Mod1.

Flat directory structure

But unlike Java or Python, where the contents of a package named bob.pkg would actually reside in a nested directory structure with the path bob/pkg, EM employs a flat organization in which the logical package-name and physical directory-name must match exactly.

Moving on, the declarations beginning at line 3 comprise the public specification of this module – a coherent collection of constants, types, and functions (what others might term an "API" ) available for direct use by clients of Mod1. Taken together, these externally-visible features of the module constitute a programmatic contract in which future changes on the part of the supplier should (hopefully!!! ) not violate prior client assumptions.

By contrast, features of Mod1 declared beginning at line 9 along with all definitions of its public and private functions beginning at line 15 remain hidden from any clients of this module. The supplier of Mod1 can consequently change the internal implementation of this module – optimizing performance or improving robustness – while maintaining a measure of "plug-compatibility" from its clients' perspective.

Separation of concerns

By enforcing crisp boundaries between clients and suppliers, EM modules encourage a software "best-practice" known as separation of concerns – organizing application functionality into discrete programmatic elements that encapsulate specific implementation decisions. Besides helping us digest software-rich systems in "bite-sized" chunks (where each EM module becomes a small world unto itself), our ability to manage change throughout the software life-cycle emerges as the most enduring benefit of modularity in general.

Importing modules

To gain access to public features of bob.pkg/Mod1, clients must explicitly import this module within their own .em files prior to any direct usage. As an illustration, consider a sample module named dave.pkg/Mod2:

dave.pkg/Mod2.em
package dave.pkg

from bob.pkg import Mod1

module Mod2       # client-visible feature declarations
    function f( ... )

private:          # supplier-proprietary feature declarations
    var t: Mod1.T
end

def f(...)        # supplier-proprietary function definitions
    Mod1.f( ... )
end

The directive at line 3 effectively adds the identifier Mod1 to this file's top-level namespace, which already includes all public / private feature names of Mod2; an optional trailing as clause can resolve name conflicts, should they arise. Through their import directives, EM modules organize themselves into a static hierarchy of clients and suppliers. As a rule, this client-supplier relation must remain acyclic; an EM module can neither directly nor indirectly import itself.

After importing Mod1, the example accesses this module's public type T at line 9 followed by its public function f at line 13 using a qualified name of the form Mod1.feature. This syntax enables client modules using Mod1 to (coincidentally) declare their own features with identical names, such as the function f defined here within the scope of Mod2; unqualified identifiers always refer to features defined within the current module.

Main programs

By implementing a special (intrinsic) function named em$run, any EM module can potentially serve as the entry-point for an executable application – a common pattern in many modern languages. Said another way, EM has no inherent notion of a "main-program"; instead, application developers will designate a particular module as the top of a client-supplier hierarchy defined via import directives.

Abstracting suppliers

At the end of the day, EM application programs comprise a set of concrete modules – each contributing some measure of encapsulated code and data to the final executable image. In the example above, where we directly imported bob.pkg/Mod1, this module will auto­matically and unconditionally become an element of any application program that directly or indirectly uses dave.pkg/Mod2.

As an alternative to this mode of direct coupling between modules, EM introduces a language construct known as a proxy that adds a level of indirection between clients and suppliers – abstracting a particular supplier's identity from the client's perspective. Akin to polymorphism within object-oriented programming, EM proxies can enhance application flexibility and improve software re-use by further decoupling clients from suppliers; the same client module, as you'll soon see, can effectively (re-)use different supplier imple­mentations of otherwise common functionality.

And even more important than re-use, we can better manage change !!!

By not having to modify client modules that employ proxies, our software becomes more resilient and malleable when – and not if – application requirements evolve over time.

To capture commonality amongst a family of "plug-compatible" modules, EM enables us to separately publish a set of client-visible features as an interface – a public specification independent of any particular supplier implementation. As an illustration, let's refactor the bob.pkg/Mod1 module presented earlier.

bob.pkg/ModI.em
1
2
3
4
5
6
7
package bob.pkg

interface ModI             # public specification only  
    const C: ...
    type T:  ...
    function f( ... )
end
bob.pkg/Mod1.em
package bob.pkg

from bob.pkg import ModI

module Mod1: ModI         # public specification  
    # additional features

private:                  # internal implementation
    # same as before
end

def f()
    # body of 'f'
end

# etc...

The declarations beginning at line 3 mimic the earlier public specification of bob.pkg/Mod1; but unlike a concrete module, an abstract EM interface cannot have an internal implementation. Our new rendition of Mod1 now inherits its public specification from the interface ModI at line 3, while declaring any additional (public) features unique to this module; the private portion of Mod1 beginning at line 8 remains unchanged from before.

More on interfaces

In practice, abstract interfaces and implementing modules will often reside in different packages: for instance, the EM runtime contains a package named  em.hal holding ConsoleUartI, GpioI, WakeupTimerI, and other interfaces; packages such as  ti.mcu.cc23xx then hold concrete implementations of these abstract interfaces targeting a particular MCU architecture.

Returning to the matter at hand – abstracting suppliers – we'll now refactor our original dave.pkg/Mod2 client module to remove its direct dependence on bob.pkg/Mod1 and instead leverage a local proxy implementing the ModI interface.

dave.pkg/Mod2.em
package dave.pkg

from bob.pkg import ModI

module Mod2       # client-visible feature declarations
    proxy ModX: ModI
    function f( ... )

private:          # supplier-proprietary feature declarations
    var t: ModX.T
end

def f(...)        # supplier-proprietary function definitions
    ModX.f( ... )
end

The syntax at line 6 adds the name ModX to the top-level scope of Mod2, as well as declares that the proxy ModX provides all client features specified within the interface ModI; access to the public type T and the public function f at lines 10 and 14 respectively mirror earlier direct usage of bob.pkg/Mod1.

Once again, we should emphasize that client Mod2 has no overt coupling to the concrete module Mod1; instead, Mod2 only knows about the abstract interface ModI – which would admit an unbounded number of alternate implementations beyond that provided by Mod1.

The proxy – interface pattern

The pattern exemplified here occurs extensively within the EM runtime, and largely holds the key to maintaining platform portability. As a case in point, the package  em.utils contains only portable modules such as AlarmMgr which declares a local proxy implementing the  em.hal/WakeupTimerI interface. Client application modules likewise desiring hardware independence can simply follow suit, using the proxy – interface pattern at each point of potential variability.

Moving on to Chapter 2, we'll now explore the process of binding the Mod2.ModX proxy to the Mod1 module.