Differences

This shows you the differences between two versions of the page.

Link to this comparison view

dss:laboratoare:06 [2019/07/01 12:10]
eduard.staniloiu
dss:laboratoare:06 [2019/07/01 15:29] (current)
eduard.staniloiu [Exercises]
Line 17: Line 17:
 **static if** is the compile time equivalent of the **if** statement. Just like the **if** statement, **static if** takes a logical expression and evaluates it. Unlike the **if** statement, **static if** is not about execution flow; rather, it determines whether a piece of code should be included in the program or not. The logical expression must be evaluable at compile time. If the logical expression evaluates to **true**, the code inside the **static if** gets compiled. If the condition is **false**, the code is not included in the program as if it has never been written. **static if** is the compile time equivalent of the **if** statement. Just like the **if** statement, **static if** takes a logical expression and evaluates it. Unlike the **if** statement, **static if** is not about execution flow; rather, it determines whether a piece of code should be included in the program or not. The logical expression must be evaluable at compile time. If the logical expression evaluates to **true**, the code inside the **static if** gets compiled. If the condition is **false**, the code is not included in the program as if it has never been written.
  
 +Let's assume we want to define a **use** function that can use an object of type **T** to **get**, **deliver** and optionally **wrap** another object (we'll use an **int** for simplicity). The code for such a function will look something like the following
 +
 +<code d>
 +void use(T)(T object) {
 +  // ...
 +  int v = object.get();​
 +  // Compute stuff on v
 +  object.wrap(v);​
 +  // ...
 +  object.deliver();​
 +  // ...
 +}
 +</​code>​
 +
 +The code above uses //​compile-time polymorphism//​. Compile-time polymorphism requires that the type is compatible with how it is
 +used by the template. As long as the code compiles, the template argument can be used with that template.
 +
 +As you can see, we don't fully respect the contract, as the use of **wrap** is not optional. To make it optional, we'll add a **static if** that checks if **T** has a **wrap** method.
 +
 +Q: How can we do this?
 +A: Simple. Let's just ask the compiler if //code// that calls **object.wrap(v)** is compilable. This is easily done in the D programming language through it's traits features.
  
 === traits === === traits ===
  
 +Traits are extensions to the language to enable programs, at compile time, to get at information internal to the compiler. This is also known as compile time reflection. The D programming language has a comprehensive set of ready to use [[https://​dlang.org/​spec/​traits.html|traits]].
 +
 +In our example, we are interested in the [[https://​dlang.org/​spec/​traits.html#​compiles|compiles]] trait.
 +
 +Have a look at the updated code:
 +
 +<code d>
 +void use(T)(T object) {
 +  // ...
 +  int v = object.get();​
 +  // Compute stuff on v
 +  static if (__traits(compiles,​ { object.wrap(42);​ }) {
 +    object.wrap(v);​
 +  }
 +  // ...
 +  object.deliver();​
 +  // ...
 +}
 +</​code>​
 +
 +Now we respect the contract: we only call the **wrap** method with an **int** if **T** defines one that is callable with an int.
 +
 +We can also use the **is** expression to achieve the same compile time check, with the following idiom:
 +<code d>
 +  static if (is(typeof({ object.wrap(42);​ }))) {
 +    object.wrap(v);​
 +  }
 +</​code>​
 +
 +Though it might look complicated,​ it's not. What happens here is that **typeof({ object.wrap(42);​ })** would be a compile-time error if we can't call **wrap**. A compile time error is not a valid type, so the result of **is(__error__)** is **false**. Go ahead, try the two **static if**s out.
  
 === Named constraints === === Named constraints ===
 +
 +Now, we respect our contract, but we can do better. If a user tries to call **use** with a type that doesn'​t have **get** or **deliver**,​ he will get a compile time error, but the error will come from the library function instead of the user call site.
 +To fix this, we need to add a template constraint
 +
 +<code d>
 +void use(T)(T object)
 +if (is (typeof(object.get())) &&
 +    is (typeof(object.deliver())))
 +{
 +  /* ... */
 +}
 +</​code>​
 +
 +Although such constraints achieve the desired goal, sometimes they are too complex to be readable. Instead, it is possible to give a more descriptive name to the whole constraint:
 +
 +<code d>
 +void use(T)(T object)
 +if (canGetAndDeliver!T)
 +{
 +  /* ... */
 +}
 +</​code>​
 +
 +That constraint is more readable because it is now more clear that the template is designed to work with types that can get and deliver. Such constraints are achieved by an idiom that is implemented similar to the following eponymous template:
 +
 +<code d>
 +template canGetAndDeliver(T) {
 +  enum canGetAndDeliver = is (typeof(
 +  {
 +    T object;
 +    object.get();​
 +    object.deliver();​
 +  }()));
 +}
 +</​code>​
 +
 +==== Mixins ====
 +
 +Mixins are for mixing in generated code into the source code. The mixed in code
 +may be generated as a template instance or a string .
 +
 +=== Template mixins ===
 +
 +Template mixins insert instantiations of templates into the code by the **mixin** keyword:
 +<code d>
 +mixin a_template!(template_parameters)
 +</​code>​
 +
 +As we will see in the example below, the **mixin** keyword is used in the definitions of template mixins as well.
 +
 +The instantiation of the template for the specific set of template parameters is inserted into the source code right where the **mixin** keyword appears.
 +
 +For example, let's have a template that defines both an array of edges and a pair of functions that operate on those edges:
 +
 +<code d>
 +mixin template EdgeArrayFeature(T,​ size_t count) {
 +  T[count] edges;
 +  void setEdge(size_t index, T edge) {
 +    edges[index] = edge;
 +  }
 +  void printEdges() {
 +    writeln("​The edges:"​);​
 +    foreach (i, edge; edges) {
 +      writef("​%s:​%s ", i, edge);
 +    }
 +    writeln();
 +  }
 +}
 +</​code>​
 +
 +That template leaves the type and number of array elements flexible.
 +For example, the **mixin** below can insert the two-element //**int** array// and the two functions that are generated by the template right inside a **struct** definition:
 +
 +<code d>
 +struct Line {
 +  mixin EdgeArrayFeature!(int,​ 2);
 +}
 +</​code>​
 +
 +As a result, **Line** ends up defining a member array and two member functions:
 +<code d>
 +import std.stdio;
 +
 +void main() {
 +  auto line = Line();
 +  line.setEdge(0,​ 100);
 +  line.setEdge(1,​ 200);
 +  line.printEdges();​
 +}
 +</​code>​
 +
 +=== String mixins ===
 +
 +Another powerful feature of D is being able to insert code as string as long as that string is known at compile time. The syntax of string mixins requires the use of parentheses:​
 +<code d>
 +mixin (compile_time_generated_string)
 +</​code>​
 +
 +For example, the //hello world// program can be written with a **mixin** as well:
 +<code d>
 +import std.stdio;
 +
 +void main() {
 +  mixin (`writeln("​Hello,​ World!"​);​`);​
 +}
 +</​code>​
 +
 +Obviously, there is no need for mixins in these examples, as the strings could have been written as code as well.
 +
 +The power of string mixins comes from the fact that the code can be generated at compile time.
 +
 +A common use case of string mixins is **Operator Overloading**:​
 +<code d>
 +struct A {
 +  int x;
 +  ​
 +  ref A opOpAssign(string op)(ref A rhs)
 +  if (op == "​+"​ || op == "​-"​)
 +  {
 +    mixin("​this.x"​ ~ op ~ "= rhs.x;"​);​
 +  }
 +}
 +
 +void main() {
 +  A a1 = A(20);
 +  A a2 = A(22);
 +  ​
 +  a1 += a2;
 +  writeln(a1);​ // -> 42
 +  a1 -= a2;
 +  writeln(a1);​ // -> 20
 +</​code>​
 +
 +Another example is represented by string predicates. Let's consider the following function template that takes an array of numbers
 +and returns another array that consists of the elements that satisfy a specific condition:
 +
 +<code d>
 +int[] filter(string predicate)(int[] numbers) {
 +  int[] result;
 +  foreach (number; numbers) {
 +    if (mixin (predicate)) {
 +      result ~= number;
 +    }
 +  }
 +  return result;
 +}
 +</​code>​
 +
 +That function template takes the filtering condition as its template parameter and inserts that condition directly into an if statement as is.
 +
 +For that condition to choose numbers that are e.g. less than 7, the if condition should look like the following code:
 +<code d>
 +if (number < 7) {
 +</​code>​
 +
 +The users of the **filter()** template can provide the condition as a string:
 +<code d>
 +int[] numbers = [ 1, 8, 6, -2, 10 ];
 +int[] chosen = filter!"​number < 7"​(numbers);​
 +</​code>​
 +
 +Importantly,​ the name used in the template parameter must match the name of the variable used in the implementation of filter() . So, the template must document what that name should be and the users must use that name.
 +
 +Phobos uses names consisting of single letters like a, b, n, etc.
 +
 +<​note>​
 +Specifying predicates as strings was used more commonly before the lambda syntax was added to D. Although string predicates as in this example are still used in Phobos, the **=>** lambda syntax may be more suitable in most cases.
 +</​note>​
 +
 +==== Exercises ====
 +
 +=== 1. checkedint ===
 +
 +We want to define a **struct CheckedInt** that defines facilities for efficient checking of integral operations against overflow, casting with loss of precision, unexpected change of sign, etc. **CheckedInt** must work with both built-in integral types and other **CheckedInt**s.
 +
 +CheckedInt offers encapsulated integral wrappers that do all checking internally and have configurable behavior upon erroneous results. For example, **CheckedInt!int** is a type that behaves like int but aborts execution immediately whenever involved in an operation that produces the arithmetically wrong result. ​
 +
 +The declaration should look something like
 +<code d>
 +struct Checked(T, Hook)
 +</​code>​
 +
 +In order to work with built-in types, you need to define: opAssign, opBinary, opBinaryRight,​ opCast, opCmp, opEquals, opOpAssign, opUnary.
 +
 +CheckedInt has customizable behavior with the help of a second type parameter, Hook. Depending on the Hook type, core operations on the underlying integral may be verified for overflow or completely redefined. Implement the following predefined hooks:
 +  - **Abort** : fails every incorrect operation with a message to std.stdio. stderr followed by a call to assert(0). It is the default second parameter, i.e. Checked!short is the same as Checked!(short,​ Abort).
 +  - **Throw** : fails every incorrect operation by throwing an exception. ​
 +  - **Warn**: prints incorrect operations to std.stdio.stderr but otherwise preserves the built-in behavior. ​
 +
 +
 +
 +
dss/laboratoare/06.1561972225.txt.gz ยท Last modified: 2019/07/01 12:10 by eduard.staniloiu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0