View mode: basic / threaded / horizontal-split · Log in · Help
September 05, 2012
How to have strongly typed numerical values?
Hello.
I am trying to work out if there is existing support for strongly 
typed numerical values for example degrees west and kilograms 
such that they cannot be accidentally mixed in an expression. I 
have vague recollection of seeing a presentation by Walter 
talking about this but I cannot seem to find it. I have looked at 
std.typecons.Typedef and Proxy but neither seem to do what I want 
or at least fail to compile for the expression n =  n + n;. I 
could easily implement my own as I have done in C++ in the past 
but assume there is a standard implementation which I would 
prefer.
Any help or links to examples much appreciated.

Regards,

Nicholas
September 05, 2012
Re: How to have strongly typed numerical values?
Nicholas Londey:

> for example degrees west and kilograms such that they cannot be 
> accidentally mixed in an expression.

Using the static typing to avoid similar bugs is the smart thing 
to do :-)


 I have vague recollection of seeing a presentation
> by Walter talking about this but I cannot seem to find it. I 
> have looked at std.typecons.Typedef and Proxy but neither seem 
> to do what I want or at least fail to compile for the 
> expression n =  n + n;.

std.typecons.Typedef is kind of broken.


> but assume there is a standard implementation which I would 
> prefer.

I think Phobos doesn't have a standard implementation.

Bye,
bearophile
September 05, 2012
Re: How to have strongly typed numerical values?
On Wednesday, 5 September 2012 at 00:55:12 UTC, Nicholas Londey 
wrote:
> Hello.
> I am trying to work out if there is existing support for 
> strongly typed numerical values for example degrees west and 
> kilograms such that they cannot be accidentally mixed in an 
> expression. I have vague recollection of seeing a presentation 
> by Walter talking about this but I cannot seem to find it. I 
> have looked at std.typecons.Typedef and Proxy but neither seem 
> to do what I want or at least fail to compile for the 
> expression n =  n + n;. I could easily implement my own as I 
> have done in C++ in the past but assume there is a standard 
> implementation which I would prefer.
> Any help or links to examples much appreciated.
>
> Regards,
>
> Nicholas

I don't know about any standard (or wide-spread) solution. I did 
my own and it works well for me.
Code follows, no rights reserved:

module quantity;

/// Examples:
unittest {
	mixin BasicUnit!"ampere";
	mixin BasicUnit!"second";
	
	// multiplication of different quantity types yields a new type
	auto coulomb = ampere * second;
	static assert(!is(typeof(coulomb) == typeof(ampere)));
	static assert(!is(typeof(coulomb) == typeof(second)));
	static assert(is(typeof(coulomb / second) == typeof(ampere)));
	real dimensionless = second / second;
	
	// quantities of the same type can be added, quantities can be 
multiplied
	// with dimensionless values
	assert(coulomb + coulomb == coulomb * 2);
	
	// can't add different types
	static assert(!__traits(compiles, ampere + second));
	static assert(!__traits(compiles, ampere + coulomb));
	static assert(!__traits(compiles, ampere + 1));
}

import std.traits: isIntegral;
import std.typetuple;

debug import std.stdio;

/** Mixin to define a basic unit with the given name.
	
	Params:
		name = must be a valid identifier
*/
mixin template BasicUnit(string name, Value = real) {
	struct Id {}
	enum q = Quantity!(UnitSpec!(Id, 1), Value)(make!Value(1));
	mixin("
		static if(is(typeof(" ~ name ~ "))) {
			static assert(false);
				//TODO: better error message
		} else {
			alias q " ~ name ~ ";
		}
	");
}
/// Examples:
unittest {
	mixin BasicUnit!"ampere";
	static assert(isQuantity!(typeof(ampere)));
}

/** A physical quantity, i.e. a numerical value and a unit of 
measurement.
	
	Do not instantiate Quantity directly. Use BasicUnit instead.
*/
private struct Quantity(UnitSpec, Value) {
	Value value; /// the (dimensionless) numerical value of the 
quantity
	
	static assert(isUnitSpec!UnitSpec);
	static assert(UnitSpec._Spec.length >= 2);
	private alias UnitSpec _UnitSpec;
	private alias Value _Value;
	
	/** Overloads for arithmetic operators.
		
		Quantities can be added to other quantities of the same type.
		Dimensionsless quantities can also be added to scalars, 
resulting in a
		scalar.
		Quantities can be multiplied by other quantities and by scalars.
	*/
	Quantity opBinary(string op)(in Quantity right) const
	if(op == "+" || op == "-") {
		mixin("return Quantity(value " ~ op ~ " right.value);");
	}
	
	/// ditto
	void opOpAssign(string op)(in Quantity right) if(op == "+" || op 
== "-") {
		mixin("value " ~ op ~ "= right.value;");
	}
	
	Quantity opUnary(string op)() const if(op == "-" || op == "+") {
		mixin("return Quantity(" ~ op ~ "value);");
	}
	
	/// ditto
	auto opBinary(string op, R)(in R right) const
	if(op == "*" || op == "/") {
		static if(is(R == Quantity)) { // => dimensionsless
			mixin("return value " ~ op ~ " right.value;");
			
		} else static if(isQuantity!R) {
			alias _UnitSpec.Binary!(op, R._UnitSpec) ResultUnitSpec;
			mixin("Value v = value " ~ op ~ " right.value;");
			return Quantity!(ResultUnitSpec, Value)(v);
			
		} else static if(is(R : Value)) {
			mixin("return Quantity(value " ~ op ~ " right);");
			
		} else {
			static assert(false);
		}
	}
	
	/// ditto
	Quantity opBinaryRight(string op)(in Value left) const
	if(op == "*" || op == "/") {
		mixin("return Quantity(left " ~ op ~ " value);");
	}
	
	/// ditto
	void opOpAssign(string op)(in Value right) if(op == "*" || op == 
"/") {
		mixin("value " ~ op ~ "= right;");
	}
	
	/** comparison with other quantities of the same type
	*/
	bool opEquals(ref const(Quantity) other) const {
		return typeid(Value).equals(&value, &other.value);
	}
	
	/// ditto
	bool opEquals(const(Quantity) other) const {
		return opEquals(other); // forward to the ref version
	}
	
	/// ditto
	int opCmp(ref const(Quantity) other) const {
		return typeid(Value).compare(&value, &other.value);
	}
	
	/// ditto
	int opCmp(const(Quantity) other) const {
		return opCmp(other); // forward to the ref version
	}
	
	///
	hash_t toHash() const nothrow @safe {
		return typeid(Value).getHash(&value);
	}
	
	///
	static @property Quantity zero() {
		return Quantity(make!Value(0));
	}
}
unittest {
	static assert(!is(Quantity!(UnitSpec!(), real)));
	mixin BasicUnit!"ampere";
	mixin BasicUnit!"second";
	auto coulomb = ampere * second;
	real dimensionless = second / second;
	static assert(!__traits(compiles, 1 + ampere));
	coulomb += coulomb;
	assert(coulomb.value == 2);
	coulomb *= 2;
	assert(coulomb.value == 4);
	assert(-ampere == -1 * ampere);
	assert(+ampere == ampere);
}

///
alias Curry!(isTemplatedType, Quantity) isQuantity;

/*
	Params:
		Spec = Alternating basic types and exponents. Basic types must 
be sorted
			by their mangled names. No duplicates allowed.
	
	This is a struct just because a type is easier to work with than 
a template.
*/
private struct UnitSpec(Spec ...) {
	private alias Spec _Spec;
	
	private template less(Lhs, Rhs) {
		enum bool less = Lhs.mangleof < Rhs.mangleof;
	}
	
	static if(Spec.length >= 2) {
		static assert(is(Spec[0]));
		static assert(is(typeof(Spec[1])));
		static assert(isIntegral!(typeof(Spec[1])));
		static if(Spec.length > 2) {
			static assert(less!(Spec[0], Spec[2]));
		}
		
		alias Spec[0 .. 2] Head;
		alias Spec[0] HeadId;
		enum headExp = Spec[1];
		alias UnitSpec!(Spec[2 .. $]) Tail;
		
		// Merges U's Spec with the current one.
		template Binary(string op, U) if(op == "*") {
			static assert(isUnitSpec!U);
			static if(U._Spec.length == 0) {
				alias UnitSpec Binary;
			} else static if(less!(HeadId, U.HeadId)) {
				alias UnitSpec!(Head, Tail.Binary!("*", U)._Spec) Binary;
			} else static if(less!(U.HeadId, HeadId)) {
				alias UnitSpec!(U.Head, Binary!("*", U.Tail)._Spec) Binary;
			} else {
				static assert(is(HeadId == U.HeadId));
				private alias Tail.Binary!("*", U.Tail) BinaryTail;
				private enum expSum = headExp + U.headExp;
				static if(expSum == 0) {
					alias BinaryTail Binary;
				} else {
					alias UnitSpec!(HeadId, expSum, BinaryTail._Spec) Binary;
				}
			}
		}
		
	} else {
		static assert(Spec.length == 0);
		
		// see above
		template Binary(string op, U) if(op == "*") {
			static assert(isUnitSpec!U);
			alias U Binary;
		}
	}
	
	// Division is multiplication by the inverse.
	template Binary(string op, U) if(op == "/") {
		static assert(isUnitSpec!U);
		alias Binary!("*", U.Inverse) Binary;
	}
	
	// Exponentiation is repeated multiplication.
	template Pow(int e) {
		static assert(e > 0);
		static if(e == 1) {
			alias UnitSpec Pow;
		} else {
			alias Binary!("*", Pow!(e - 1)) Pow;
		}
	}
	
	// Negates the exponents.
	static if(Spec.length == 0) {
		alias UnitSpec Inverse;
	} else {
		alias UnitSpec!(_Spec[0], -1 * _Spec[1], Tail.Inverse._Spec) 
Inverse;
	}
}
unittest {
	static struct AmpereId {}
	static struct SecondId {}
	alias UnitSpec!(AmpereId, 1) Ampere;
	alias UnitSpec!(SecondId, 1) Second;
	alias UnitSpec!(SecondId, -1) Hertz;
	alias UnitSpec!(AmpereId, 1, SecondId, 1) Coulomb;
	
	static assert(is(Second.Binary!("*", Ampere).Binary!("*", Ampere)
		== UnitSpec!(AmpereId, 2, SecondId, 1)));
	
	static assert(is(Second.Inverse == Hertz));
	
	// no types
	version(none) static assert(!__traits(compiles, UnitSpec!(1, 
1)));
		/*NOTE: dmd 2.060 complains even though this is supposed to fail
		compilation */
	
	// no exponents
	static assert(!__traits(compiles, UnitSpec!(SecondId, 
AmpereId)));
	
	// not ordered alphabetically
	static assert(!__traits(compiles, UnitSpec!(SecondId, 1, 
AmpereId, 1)));
}

private alias Curry!(isTemplatedType, UnitSpec) isUnitSpec;

/** Exponentiation for quantities.
	
	The exponent has to be known at compile time, because it changes 
the
	result type.
*/
auto pow(int exponent, Q)(Q quantity) if(isQuantity!Q) {
	alias Q._UnitSpec.Pow!exponent U;
	return Quantity!(U, Q._Value)(quantity.value ^^ exponent);
}
/// Examples:
unittest {
	mixin BasicUnit!"meter";
	assert(pow!3(meter) == pow!2(meter) * meter);
}
/// the same as pow!2(quantity)
auto square(Q)(Q quantity) if(isQuantity!Q) {
	return pow!2(quantity);
}
/// the same as pow!3(quantity)
auto cubic(Q)(Q quantity) if(isQuantity!Q) {
	return pow!3(quantity);
}
unittest {
	mixin BasicUnit!"meter";
	assert(cubic(meter) == square(meter) * meter);
}


// below: various assorted helper functions

// initialize an assignable type to x
T make(T)(int x) {
	T result;
	result = x;
	return result;
}

/**
	Doesn't work on function templates.
*/
template Curry(alias Template, Args ...) {
	template Curry(MoreArgs ...) {
		alias Template!(Args, MoreArgs) Curry;
	}
}
/// Examples:
unittest {
	static int foo(int a, int b)() {
		return a + b;
	}
	alias Curry!(foo, 42) bar;
	assert(bar!1() == 43);
}

/** If Type is an instance of Template, get the template 
parameters.
*/
template TemplateParameters(alias Template, Type) {
	static if(is(Type _ == Template!Args, Args ...)) {
		alias Args TemplateParameters;
	} else {
		alias void TemplateParameters;
	}
}
/// Examples:
unittest {
	struct S(P ...) {}
	static assert(is(TemplateParameters!(S, S!(int, float)) ==
		TypeTuple!(int, float)));
	
	struct T(P ...) {}
	static assert(is(TemplateParameters!(T, S!(int, float)) == 
void));
}

/** true if Type is an instance of Template
*/
template isTemplatedType(alias Template, Type) {
	enum isTemplatedType = !is(TemplateParameters!(Template, Type) 
== void);
}
/// Examples:
unittest {
	struct A() {}
	struct B() {}
	
	static assert(isTemplatedType!(A, A!()));
	static assert(!isTemplatedType!(A, B!()));
	
	alias Curry!(isTemplatedType, A) isA;
	
	static assert(isA!(A!()));
	static assert(!isA!(B!()));
}
September 05, 2012
Re: How to have strongly typed numerical values?
On 09/04/2012 05:55 PM, Nicholas Londey wrote:

> I could easily implement my own as I have done in C++ in the
> past but assume there is a standard implementation which I would prefer.
> Any help or links to examples much appreciated.

UFCS enables some interesting syntax:

struct Grams
{
    size_t amount;
}

@property Grams grams(size_t amount)
{
    return Grams(amount);
}

void main()
{
    auto weight = 5.grams;
}

C++11 brings similar conveniences as well.

Ali
September 05, 2012
Re: How to have strongly typed numerical values?
Using this:

struct Grams
{
    size_t amount;
}

@property Grams grams(size_t amount)
{
    return Grams(amount);
}

void main()
{
    auto weight = 5.grams;
    weight = weight + 10.grams;
}

How would you use it? I thought the point of this sort of strong 
typing was to be able to carry out arithmetic using your type 
that other units cannot accidentally be mixed with.
September 05, 2012
Re: How to have strongly typed numerical values?
On 09/04/2012 08:11 PM, ixid wrote:
> Using this:
>
> struct Grams
> {
> size_t amount;
> }
>
> @property Grams grams(size_t amount)
> {
> return Grams(amount);
> }
>
> void main()
> {
> auto weight = 5.grams;
> weight = weight + 10.grams;
> }
>
> How would you use it? I thought the point of this sort of strong typing
> was to be able to carry out arithmetic using your type that other units
> cannot accidentally be mixed with.

I have not used such frameworks myself but have seen them mentioned a 
number of times. I think the type above should have been Weight, not Grams.

The idea is to build the relationships between types and allow only 
those operations. For example, the following program defines dividing 
Distance by Time to produce Speed:

import std.stdio;
import std.string;

struct Distance
{
    double amount;

    Speed opBinary(string op)(in Time time) const
        if (op == "/")
    {
        return Speed(this.amount / time.amount);
    }
}

@property Distance meters(double amount)
{
    return Distance(amount);
}

struct Time
{
    double amount;
}

@property Time seconds(double amount)
{
    return Time(amount);
}

struct Speed
{
    double amount;

    string toString() const
    {
        return format("%s m/s", amount);
    }
}

void main()
{
    auto distance = (10.5).meters;
    auto time = (2.5).seconds;
    auto speed = distance / time;

    writeln(speed);
}

The parentheses around the floating point literals are not necessary. I 
thought this reads better.

Ali
September 05, 2012
Re: How to have strongly typed numerical values?
On Wed, 05 Sep 2012 02:55:45 +0200, Nicholas Londey <londey@gmail.com>  
wrote:

> Hello.
> I am trying to work out if there is existing support for strongly typed  
> numerical values for example degrees west and kilograms such that they  
> cannot be accidentally mixed in an expression. I have vague recollection  
> of seeing a presentation by Walter talking about this but I cannot seem  
> to find it. I have looked at std.typecons.Typedef and Proxy but neither  
> seem to do what I want or at least fail to compile for the expression n  
> =  n + n;. I could easily implement my own as I have done in C++ in the  
> past but assume there is a standard implementation which I would prefer.
> Any help or links to examples much appreciated.
>
> Regards,
>
> Nicholas

Not mine, but this is the implementation I use:

https://github.com/klickverbot/phobos/tree/units/std

Files are units.d and si.d.

Documentation:
http://klickverbot.at/code/units/std_units.html
http://klickverbot.at/code/units/std_si.html

-- 
Simen
September 05, 2012
Re: How to have strongly typed numerical values?
On Wednesday, 5 September 2012 at 05:03:38 UTC, Simen Kjaeraas 
wrote:

> Not mine, but this is the implementation I use:
>
> https://github.com/klickverbot/phobos/tree/units/std
>
> Files are units.d and si.d.
>
> Documentation:
> http://klickverbot.at/code/units/std_units.html
> http://klickverbot.at/code/units/std_si.html

Fascinating how similar this looks to my (completely independent) 
implementation.

I noticed two flaws in std.units:
1) Can't add quantities of the same type (this is probably 
trivial to fix).
2) Different scopes don't make different quantity types.
September 05, 2012
Re: How to have strongly typed numerical values?
On 05/09/12 03:42, bearophile wrote:
> Nicholas Londey:
>
>> for example degrees west and kilograms such that they cannot be
>> accidentally mixed in an expression.
>
> Using the static typing to avoid similar bugs is the smart thing to do :-)

I'd be interested to know if that idea is ever used in real code. I 
mean, it's a classic trendy template toy, but does anyone actually use it?

I say this because I've done a lot of physics calculation involving 
multiple complicated units, but never seen a use for this sort of thing.
In my experience, problems involving units are easily detectable (two 
test cases will catch them all).
The most insidious bugs are things like when you have used constants at 
25'C instead of 20'C, or when you have a sign wrong.
September 05, 2012
Re: How to have strongly typed numerical values?
Don Clugston:

> I'd be interested to know if that idea is ever used in real 
> code. I mean, it's a classic trendy template toy, but does 
> anyone actually use it?

As usual I don't have usage statistics.

I like dynamic languages, like Python. But if you give me a 
static type system, then I want something back from the language. 
Detecting certain bugs at compile-time is one way to pay me back.

If you want most programmers to use a feature like units, it 
needs:
- Built-in, in the standard library, or easy to install;
- To have a compact and very nice looking syntax;
- Easy enough to use;
- Flexible, to be able to represent all units you find in many 
real problems;
- Have zero or near-zero run-time costs, in both CPU and memory;
- Give acceptable error messages.

I think in F# people are using units often enough.

If a units system will be present in Phobos and it will be very 
good to use, maybe D programmers will use them.

I am currently using Haskell a little, and I am enjoying giving 
types to values to tell them apart in terms of their usage.


> I say this because I've done a lot of physics calculation 
> involving multiple complicated units, but never seen a use for 
> this sort of thing.
> In my experience, problems involving units are easily 
> detectable (two test cases will catch them all).
> The most insidious bugs are things like when you have used 
> constants at 25'C instead of 20'C, or when you have a sign 
> wrong.

There are some famous problems caused by mixing units:
http://lamar.colostate.edu/~hillger/unit-mixups.html

Maybe this is the most famous:
http://mars.jpl.nasa.gov/msp98/news/mco990930.html

Bye,
bearophile
« First   ‹ Prev
1 2
Top | Discussion index | About this forum | D home