简体   繁体   中英

What programming languages have something like Haskell’s `newtype`

The Haskell programming language has a concept of newtypes : If I write newtype Foo = Foo (Bar) , then a new type Foo is created that is isomorphic to Bar , ie there are bijective conversions between the two. Properties of this construct are:

  • The two types are completely separate (ie the compiler will not allow you to use one where the other is expected, without using the explicit conversions).
  • They share the same representation. In particular, the conversion functions have a run-time cost of zero and return ”the same object” on the heap.
  • Conversion is only possible between such types and cannot be mis-used, ie type safety is preserved.

What other programming languages provide this feature?

One example seems to be single-value-structs in C when used with record accessors/constructors only. Invalid candidates would be single-valued-structs in C when used with casts, as the casts are not checked by the compiler, or objects with a single member in Java, as these would not share the same representation.

Related questions: Does F# have 'newtype' of Haskell? (No) and Does D have 'newtype'? (not any more).

Frege has this, though, unlike in Haskell there is no extra keyword. Instead, every product type with just one component is a newtype.

Example:

data Age = Age Int

Also, all langugaes that have nominal typing and allow to define a type in terms of another should have this feature. For example Oberon, Modula-2 or ADA. So after

type age = integer;      {* kindly forgive syntax errors *}

one couldn't confuse an age and some other quantity.

I believe Scala's value classes satisfy these conditions.

For example:

case class Kelvin(k: Double) extends AnyVal

Edit: actually, I'm not sure that conversions have zero overhead in all cases. This documentation describes some cases where allocation of objects on the heap is necessary, so I assume in those cases there would be some runtime overhead in accessing the underlying value from the object.

Go has this:

If we declare

 type MyInt int var i int var j MyInt 

then i has type int and j has type MyInt. The variables i and j have distinct static types and, although they have the same underlying type, they cannot be assigned to one another without a conversion.

"The same underlying type" means that the representation in memory of a MyInt is exactly that of an int . Passing a MyInt to a function expecting an int is a compile-time error. The same is true for composite types, eg after

type foo struct { x int }
type bar struct { x int }

you can't pass a bar to a function expecting a foo ( test ).

Mercury is a pure logic programming language, with a type system similar to Haskell's.

Evaluation in Mercury is strict rather than lazy, so there would be no semantic difference between Mercury's equivalents of newtype and data . Consequently any type which happens to have only one constructor with only one argument is represented identically to the type of that argument, but still treated as the same type; effectively "newtype" is a transparent optimisation in Mercury. Example:

:- type wrapped
    --->    foo(int)
    ;       bar(string).

:- type wrapper ---> wrapper(wrapped).

:- type synonym == wrapped.

The representation of wrapper will be identical to that of wrapped but is a distinct type, as opposed to synonym which is simply another name for the type wrapped .

Mercury uses tagged pointers in its representations. 1 Being strict and being allowed to have different representations for different types, Mercury generally tries to do away with boxing where possible. eg

  • To refer to a value of an "enum-like" type (all nullary constructors) you don't need to point to any memory so you can use a whole word's worth of tag bits to say which constructor it is and inline that in the reference
  • To reference a list you can use a tagged pointer to a cons cell (rather than a pointer to a structure which itself contains the information about whether it is nill or a cons cell)
  • etc

The "newtype" optimization is really just one particular application of that general idea. The "wrapper" type doesn't need any memory cells allocated above what is already holding the "wrapped" type. And since it needs zero tag bits, it can also fit any tags in the reference to the "wrapped" type. Therefore the whole reference to the "wrapped" type can be inlined into the reference to the wrapper type, which ends up being indistinguishable at runtime.


1 The details here may only apply to the low level C compilation grades. Mercury can also compile to "high level" C or to Java. There's obviously no bit fiddling going on in Java (though as far as I know the "newtype" optimization still applies), and I'm just less familiar with the implementation details in the high level C grades.

Rust has always allowed you to make single-field types, but with the recently stabilized repr(transparent) attribute you can now be confident that the type created will have the exact data layout as the wrapped type, even across FFI and such.

#[repr(transparent)]
pub struct FooWrapper(Foo);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM