← Back to team overview

n-able-devel team mailing list archive

What is a class?

 

It seems to me that a class is two things:
  1.. the definition of a specification. 
  2.. the creation of an implementation that meets that specification.
Other code, then, can expect that it be given something which meets that specification (be passed an object instance of that class), and it can make calls to that implementation in order to do something.

Inheritance, then is two things:
  1.. stating that one specification is an extension, forever more, of another. 
  2.. a way to state that a second implementation also meets a given specification.
In most languages, these concepts are all conflated. This causes difficulties. For example:
  a.. each specification can have only one base implementation. In order to have a second implementation, we need additional language constructs (inheritance or generics). 
  b.. inheritance allows changing both the specification and the implementation. Thus, if A inherits from B, we cannot say whether A and B are covariant or contravariant or both. This limits reasoning about programs. We can only say that a function is covariant on its args & contrvariant on its return type. There is no way to just add one more implementation, nor is there a way to just extend the specification. 
  c.. for some classes, it is important that their use sites not be indirection points. However, we still want them to be modular for reuse. For example, security concerns in .Net mean that we want to make sure that the classes implementing encryption protocols not be indirection points. Otherwise, code could perform a man in the middle attack on other code. The ways to do this (eg, making the class sealed) require additional language support and are a blunt force instrument. They inhibit testability and other concerns. 
  d.. because inheritance allows all types of extensions, it becomes the go-to solution for extension. However, composition gives greater flexibility to the resulting program. This makes applications harder to change. 
  e.. similarly, the compiler can’t make as many assumptions about your intent when you use inheritance, so it can’t do as much compile-time checking for you.
Many languages thus offer a way to declare a specification without also declaring an implementation (eg, a C# interface). However, these behave differently from the specifications that are defined by creating a class. When the needs of the application change only slightly, specifications need to change between these two types of specification. Although this is not a real change to the application’s intent, it results in rippling changes to the code.

Furthermore, there is often a need to declarative metadata. As most languages lack a way to express this directly, classes and their static elements (constants mostly) get used for this purpose. It works, but it isn’t that expressive. It can also be difficult to extend. If a library wants the application to supply metadata, how does it ask for it?

I propose that N-Able split the definitions of these syntactical constructs. We will provide `class` as syntactical sugar (see below).

keyword: `spec`

>>> spec Foo:
...     def string, int do_something(Bar arg1, _ arg2, _* args)
...     def _ __add__(Foo other)

This defines a specification named Foo. Foos overload the + operator for some reason, and also have do_something capability (which is a function that returns multiple values).

keyword: `new_type`

>>> new_type Frog:
...     def string, int jump():
...         return “hi”, 3
...     def _ speak():
...        return Sound.from_file(“ribbit.wav”)

Note that the type Frog does not meet any specifications. Thus, although you can construct an instance and pass it around, you won’t be able to call anything on the instance.

keyword: `aliases`:

>>> Toad aliases Frog
>>> assert.that(Toad(), is.instance_of(Frog))

Now Toad and Frog can be used interchangeably.

keyword: `decorates`:

>>> LargeFrog decorates Frog(f):
...     def _ speak():
...         return f.speak().volume_times(2)

You can now create LargeFrogs or just normal Frogs. These are different types.

keyword: `patch`:

>>> patch Frog:
...     def swim():
...         this.speak()
...         this.jump()

Within the context for which this patch applies (the scope it is defined in or any scope that specifically imports the definition), Frogs now support a .swim() method that returns void. This applies only to that context. The definition of Frog is not changed. Rather, whenever we see a Frog in this context, the compiler will treat it as if it had the swim method.

keyword: `implements`:

>>> Frog implements Foo:
...     def _ __add__(Foo other):
...         return this
...     do_something = adapt(this.jump)

Now Frog implements the Foo specification (as does Toad, but not LargeFrog). It can be passed around in Foo-type variables.

>>> LargeFrog implements Foo:
...     using.implementation(Frog, Foo)

Now LargeFrog implements Foo as well (using the mapping that we defined for Frog). We import an implementation while defining another one to say that we are basing our implementation on that one.

>>> declaration PersistFrogs(orm.MappingDefinition(Frog f)):
...     __table__ = db.Animals.join(db.Amphibians, db.by_db_relationship)
...     animal_type = “Frog”
...     db_col_swim_speed, run_speed, i_hunger = f.swim_speed, f.run_speed, f.hunger

This is a declaration of metadata. It wouldn’t compile as a class (other language or N-Able). Instead, it is parsed by orm.MappingDefinition. That definition knows to look for declarations like a __table__ assignments, a bunch of other assignments that map fields to db columns, and so on. The resulting metadata construct can then be passed around and used in various ways. Eg:

>>> @test
... @compile_time
... def verify_mappings():
...     assert.that(PersistFrogs, orm.maps_to_database(conn_str))

Dunno about all the syntax stuff here. However, I like the idea of splitting all this stuff out. Explicit over implicit, and all that.

Arlo