Dynamic typing

From D Wiki
Revision as of 17:02, 10 May 2017 by Vladimir Panteleev (talk | contribs) (Formatting)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Dynamic typing is when a variable has a runtime type tag instead of a specific compile time type. D itself is statically typed, but it is possible to implement dynamic types in the library, that work like they do in languages like Javascript (or otherwise) with only one caveat: going from dynamic back to static typing cannot be done implicitly.

Another axis of typing is strong or weak. Strong typing makes it an error to mix incompatible types. This is the case in D. Weak typing tries to make things work, even if the types are incompatible, by automatically doing conversions.

Phobos' std.variant offers a kind of strong dynamic typing. Here, though, I want to talk about weak dynamic typing in D. Can we make valid D code that looks like Javascript?

Yup. https://github.com/adamdruppe/misc-stuff-including-D-programming-language-web-stuff/blob/master/jsvar.d

import arsd.jsvar;
import std.stdio;

void main()
{
	var a = 10;
	var b = a - 5;
	a = [1,2,3];
	a[1] = "two";
	writeln(a);

	a = json!q{
		"foo":{"bar":100},
		"joe":"joesph"
	};

	writeln(a.joe);
	a.foo.bar += 55;
	b = a;
	writeln(b.foo.bar);

	var c = (var d) { writeln("hello, ", d, "!"); };
	c("adr");
	c(100);
}

The way it works is arsd.jsvar defines a struct called var that is a tagged union with a number of overloaded, templated operators:

  • opCall is a template that takes any kinds of arguments, and converts them into a var[], which is passed to the function (if there is one).
  • opAssign, which is called from the var constructor as well, takes any arguments and tags the struct based on the passed type, using compile time reflection. Conversions are also done in this template if needed.

Two interesting cases are assigning functions or structs. Functions are automatically wrapped in a helper delegate that converts a var[] arguments into the static types the original function expects (or the function can just take var, as we saw in this example). The wrapper also converts the function's return value to a var, for consistency.

Structs iterate over their members and recursively convert all them to vars as well.

  • opDispatch does the dot members, as well as opIndex, like in JavasSript. The opDispatch returns a ref to the member held inside, so you can use various assign operators on it.
  • The json!q{} literals are actually just a string literal (D has q{ ... } built in). The json!string template actually forwards to var.fromJson(), which just parses the string, with {} wrapping it so you don't have to write that twice as the q{} already has it, anyway it is parsed, at runtime right now due to a limitation in ctfe, and returns it. No real magic there.
  • Other operators have static if checks on static types and operator names, and if(payloadType()) checks on the tagged unions themselves. The logic determines the new type from the operand types and operation: the ~ operator always works on strings or arrays. The &|^ operators only work on ints. Other arithmetic always works on ints or floats - which one it brings has rules like D itself.

I also wrote a little script interpreter that uses the var struct as its main type.

https://github.com/adamdruppe/misc-stuff-including-D-programming-language-web-stuff/blob/master/script.d

It has a simple parser and interpreter that more or less just forwards script operations to D. For example, the BinaryExpression's interpret function looks like this:

override InterpretResult interpret(PrototypeObject sc)
{
	var left = e1.interpret(sc).value;
	var right = e2.interpret(sc).value;
  
	//writeln(left, " "~op~" ", right);
  
	var n;
	foreach(ctOp; CtList!("+", "-", "*", "/", "==", "!=", "<=", ">=", ">", "<", "~", "&&", "||", "&", "|", "^"))
		if(ctOp == op)
		{
			n = mixin("left "~ctOp~" right");
		}
  
	return InterpretResult(n, sc);
}

As you can see, it just loops through the operators it knows, then does a runtime check: if that is the operator they wanted, run the code, which uses mixin to forward it all straight to D. Note that the mixin is done for every one of those operators, so this is a nice compile check for var's overloading too.

What's great is that since the script and D use the same type and mostly the same syntax, interoperation is pretty easy.

auto globals = var.emptyObject;
// these global variables will be available to the script
globals.lol = 100;
globals.rofl = 23;
  
// and functions too
globals.write = (var a) { writeln("script said: ", a); };
  
// read script from file
import std.file;
  
// this can contain: var c = function(a) { write(a); };
writeln(interpret(readText("scripttest_code.d"), globals));
  
// or read script interactively from stdin
repl(globals);
  
writeln("BACK IN D!");
globals.c()(10); // call script defined functions in D

The extra () on globals.c is because @property is still broken in D so that is needed to tell D you want to call the returned function, not just fetch a reference to the function.

But as you can see, the script manipulates the same globals var you passed to it, so all that stuff is still available in D too, returning vars that you can continue to use.

I think talking more about the script language is off topic here, but check the code if you're interested.