Skip to content

Instantly share code, notes, and snippets.

@rikkimax
Last active November 23, 2018 14:02
Show Gist options
  • Save rikkimax/dd3aa22f99f66a41fae69ebe07419548 to your computer and use it in GitHub Desktop.
Save rikkimax/dd3aa22f99f66a41fae69ebe07419548 to your computer and use it in GitHub Desktop.
signature overall description

Signature type

Field Value
DIP: (number/id -- assigned by DIP Manager)
Review Count: 0 (edited by DIP Manager)
Author: Richard Andrew Cattermole [email protected]
Implementation: (links to implementation PR if any)
Status: Will be set by the DIP manager (e.g. "Approved" or "Rejected")

Abstract

A signature is a static vtable representation of complex objects such as structs and classes. They primarily represent heap allocated objects but can take advantage of the stack for scope.

They are very good at presenting behavior and description of objects in a way familiar to D programmers while limiting template if constraints that is hard to read and understand. The addition could lead to cleaner more interchangable code, as well as seeing a higher uptake in -betterC codebases.

Reference

Signatures are a very uncommon language feature originating in the ML family, examples of this are Ocaml and SML.

A more recent example of signatures is Rust's traits or Swift's Protocols which both focus upon implementing a specification, not a specification matching an existing representation.

Alternative designs to D's structs is C#, which can inherit interfaces but not other structs or classes.

A design in the C++ community include a type verification mechanism in the form of concepts. Discussion in the D NewsGroup along with a WIP DIP for it. As an addition to static_if proposal.

Supplementary code is provided by the author in the repository.

Contents

Rationale

In the D community we utilize idioms and design patterns which emphasize template usage. Template usage enables high code reuse but it also causes code bloat with quite horrendous error messages. These supplementary outcomes are not positive for both new and existing users in understanding why a template does not succeed.

At the core of the problem, we aim to pass around types based upon a statically known interface without knowledge of what the implementation is during compilation. It is quite common when template conditions fail at a function level, to not know if it is because of a function specific specialization failure or a more general interface one of the arguments.

The goal of signatures in this DIP is to present a language construct that is familiar to the (S)ML developers but also ties into how it would be used by the D community. So in our case, signatures are stack heavy constructs that can be made from either structs or classes. They have a statically created virtual table that may be assigned functions at runtime with patching to make an implementation match the interface. This allows a runtime decision of the implementation but a static compile-time design for what it must describe.

Methods exist that do similar things like Design by Introspection. They do not become obsolete instead, they become stronger more useful in customizing behavior using more concrete types. Allowing for cleaner interfaces and most importantly, a focus on concepts not code "versions" of them.

Description

Semantics

  1. Construction: Is implicitly constructed given an appropriate class or struct instance.
    1. Implicit construction of a signature may occur during assignment or passing as an argument to a function call.
    2. Implicit construction works in two forms. First as direct assignment and patching for class instances and pointers to structs. Second is a memory allocation and move of the struct instance, should it be copyable. Where it will be assigned and then patched for the given vtable.
    3. If the implicit construction is going into a scope'd variable, no allocation needs to take place unless it is being returned from a function. If it is being returned from a function it will be malloc'd and then free'd when it goes out of scope.
    4. Should a struct instance not be going into a scope'd variable (or being returned without scope) it will be allocated into GC owned memory where it will be moved into, should it be copyable. If it cannot be copied, it is an error.
    5. If the signature has named parameters (and only named parameters), they will be inferred automatically as part of construction from an implementation.
  2. Members are virtual (except static methods):
    1. There may be fields and methods. These are pointers/delegates.
    2. Operator overloads may be used.
    3. Types, alias and enum work as is with existing structs and classes.
    4. @safe field/method may be assigned to a method without any attribution. Same with @trusted, nothrow, @nogc, const and shared. Different linkage/calling conventions are supported by the compiler creating a small patch function.
    5. A final method cannot be overriden by a child signature and ignores the implementation.
    6. An override method overrides a parent signatures declaration but does not replace the implementations or a final parent method.
    7. A method maybe have multiple template if function bodies. And may refer to typeof(this). The first that is valid will be used as the method. If none match then the default (non-constraint) function body will be used.
  3. this will not refer to anything outside of a function body or constraint, and it must be virtual or it will be an error.
  4. A signature may inherit from others, similar in syntax to interfaces in this manner. However the diamond problem is not valid with signatures. If a field/method is duplicated and is similar, it can be ignored. If it is different (e.g. different types or different attributes) then it is an error at the child signature.
  5. The first pointer a signature instance stores is to the data it represents. This can be accessed by .ptr the same as a slice. If this pointer is null, so is the signature instance. To check for this use v is null.
  6. Signatures may be cast up for their inheritence. This can be computed statically and does not require any runtime knowledge. However they may not be cast down again. Rules regarding const, immutabe, shared ext. still apply like any other type.
  7. Usage of signatures as return types and arguments obey the same rules associated with interfaces and classes inside said interfaces/classes hierachies which is covariance and invariance. Patch functions will be required to implement this however and have each version available for casting to parent signatures.
  8. .sizeof property on signatures, total size of signature instance, works identically to a struct's .sizeof property.
  9. .offsetof works on each member field and method (with help of __traits(getOverloads)) of a signature.
  10. Named parameters will support signatures as if it was a class or struct.
  11. The is expression in the form of is(T == Signature) will perform an exact match on a signature type (must have been template initialized if required). When the is expression is in the form of is(T : Signature) it will attempt to initialize the Signature to T as if it was an implementation. The following code must not fail Signature(new T) for when is(T : Signature) is true.
  12. For template parameters is(T : Signature) will have the same behavior as void func(T : Signature)(T). Same with is(T == Signature) variant.

Grammar

Keyword:
+    signature

TypeSpecialization:
+    signature

AggregateDeclaration:
+    SignatureDeclaration

+ SignatureDeclaration:
+    signature Identifier AggregateBody
+    signature Identifier : SignatureInheritance AggregateBody
+    SignatureTemplateDeclaration

+ SignatureInheritance:
+    Identifier
+    Identifier , SignatureInheritance

+ SignatureTemplateDeclaration:
+    signature Identifier TemplateParameters Constraint|opt : IdentifierList AggregateBody
+    signature Identifier TemplateParameters : IdentifierList Constraint|opt AggregateBody

+FuncDeclarator:
-    StorageClasses|opt BasicType FuncDeclarator FunctionBody
+    StorageClasses|opt BasicType FuncDeclarator FunctionBody FuncDeclaratorSignature|opt

+FuncDeclaratorSignature:
+    Constraint FunctionBody FuncDeclaratorSignature|opt

Breaking Changes and Deprecations

The primary breaking change is the token signature is becoming a keyword. Existing code that uses it would need to rename existing symbols and variables but nothing requiring major changes.

Copyright & License

Copyright (c) 2018 by the D Language Foundation

Licensed under Creative Commons Zero 1.0

Review

The DIP Manager will supplement this section with a summary of each review stage of the DIP process beyond the Draft Review.

An object is fundamentally a type that has both properties and behavior associated with it.
Properties come in three forms: fields, types and constants.
The object itself is just a descriptor of the thing. It doesn't actually own the behavior or the properties.
Objects themselves are only temporary and shouldn't be kept around much.
They are there only to give a window into the type instance.
Sometimes we may want to tie the ownership of the type instance to the view, but most of the time we don't.
It is controlled else where by somebody who understands the type better than the one working with it.
Because objects are only a view on to another type, it doesn't really make much sense for them to be able to access non-public members.
Unless they themselves becomes private to the module for initialization.
This allows a module to export a view to other modules which access private data which prevents other modules trying to override visiblity protection.
When tieing ownership to a view, you must combine it with scope and ref at the initialization site.
Alternatives include a garbage collector, manual memory management and of course ref counting using special operator overloads.
Choosing which one that is used is dependent upon the initialization of the view.
Either you pass a pointer (or reference for a class), new it, use scope ref attributes as part of returning and of course if your type includes the required special operator overloads.
It is highly recommended that a view is passed around by ref and also preferably scope. This limits the cost to almost free.
However you are free to place a copy into the heap if you so wish.
The layout of a view is as follows: a pointer to some context, pointers to each field and function pointers of each method.
The properties types and constants are internal to the type but external to the view.
This means they are compile time arguments to the view but can be inferred from the type instance during initialization.
They customize the behavior and description of the view without modifying the view description itself.
However because a view is describing something else, the types and constants must be exposed as part of the definition of the view.
In D this can be done through the use of named template parameters. Preferably without the use of unnamed template parameters.
The basics of this has been given many names over the years. From signatures to protocols and traits.
We will be using the word signature because for the most part we are describing behavior or something else.
And not our own special signature type.
So far we have described the majority of a signatures behaviors. There is one very important one left to go however.
Being able to override the type instance's own behavior. This is the definition of methods default or overriden behavior.
If a method has a function body then should the type instance not have one, that function member it will default to the one of the signature.
But if we place and override attribute on it in the signature, then no lookup into the type instance will occur.
If you access this within a function body of a signature, it will refer to the type instance not the signature instance.
If you typeof(this) inside a signature anywhere, it will return the type of the instance not the signature's.
Signatures can inherit from other multiple signatures.
Unlike interfaces, they cannot suffer from the diamond problem.
Because there is only one implementation which symbol lookup occurs on.
So if multiple definitions of the same symbol occur that are not the same, it will error.
One other good thing about signatures is that can take a really restrictive type and make it less restrictive.
So if the type instance is marked with @nogc throughout but the signature isn't, then it will still work ok.
As if it wasn't annotated.
The same can occur for delegate members being usable as methods on the signature.
The compiler will create patch functions and if required turn the context of the signature instance into a special one.
It will of course match the allocation scheme except for manual memory management, where it will print the patch context required so the developer can handle it manually.
This is all well and good, but what if you want to interact with C?
Well why not throw an extern(C) onto the signature itself.
This will force all methods to be patched as required as extern(C).
It will unfortunately mean that it must have its memory managed manually.
Which will result in a special free method being generated which will call destructors and of course deallocate all memory automatically when called for you.
How nice!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment