Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: Strong Types  (Read 2715 times)

0 Members and 1 Guest are viewing this topic.

Geheim

  • Full Member
  • ***
  • Posts: 201
    • View Profile
    • Email
Strong Types
« on: September 01, 2019, 07:09:59 pm »
Hey guys,
I recently stumbled upon Jonathan Boccara's blog and especially Strong Types peaked my interest. To quote Jonathan's readme on github:

Quote
A strong type is a type used in place of another type to carry specific meaning through its name.
[...]
Strong types are about better expressing your intentions, both to the compiler and to other human developers.

Have you heared about Strong Types before? Do you use something similar in your own projects and if so, how? Do you think SFML could benefit from them?
I think they are really interesting and can make code more expressive. What do you think?
Failing to succeed does not mean failing to progress!

eXpl0it3r

  • SFML Team
  • Hero Member
  • *****
  • Posts: 10815
    • View Profile
    • development blog
    • Email
Re: Strong Types
« Reply #1 on: September 09, 2019, 11:00:43 pm »
I think the example provided isn't the greatest and I think few will ever want dedicated Width and Height types. Such boilerplating is probably more reserved for saftey critical code.

Not sure if this still belongs in the category of "strong types", however I think strong typing certain values or value groups makes totally sense and allows you to provide nice operations on top. Like the sf::Time is essentially nothing other than a sf::Int64, but instead of using that basic type directly, it's wrapped in a "strong type" which also allows to wrap the functions inside the same class or adjacent to it.
Similarly a 2D vector is just two values of the same type, so it makes sense to group them as a sf::Vector2<T> instead of the plain values (except SFML still does that in many places).

Also very useful are tools that support you with parameter information. For example Visual Studio will nicely list your the parameters needed. Or ReSharper will show you inline parameter hints (or is that just C#?):

Official FAQ: https://www.sfml-dev.org/faq.php
Official Discord Server: https://discord.gg/nr4X7Fh
——————————————————————
Dev Blog: https://duerrenberger.dev/blog/

Elias Daler

  • Hero Member
  • *****
  • Posts: 599
    • View Profile
    • Blog
    • Email
Re: Strong Types
« Reply #2 on: September 11, 2019, 11:09:51 am »
While parameter hints can save you at times, this doesn't enforce it and the mistake can be made accidental and can be quite hard to spot. It's a shame that C++ doesn't have a simple way to do strong typedefs.
Tomb Painter, Re:creation dev (abandoned, doing other things) | edw.is | @EliasDaler

Nexus

  • SFML Team
  • Hero Member
  • *****
  • Posts: 6286
  • Thor Developer
    • View Profile
    • Bromeon
Re: Strong Types
« Reply #3 on: September 11, 2019, 03:34:01 pm »
Strong types is a not so great name for two separate idioms:
  • Named arguments: naming arguments at call site (not parameters at definition site).
    Example: Rectangle(width=3, height=2)
  • Opaque type (sometimes strong typedef): a distinct type mimicking an existing one. This enhances type safety by limiting the domain in which a type can be used.
    Example: type ItemCount = int
Both ideas are not new; they have been discussed in C++ over decades, and there have been various proposals. Boost tries emulate both, in a very ugly and compile-time-intensive manner as usual. No real solution.

I think named arguments are great. I was not missing them in C++, but after using languages that support named arguments, it's often limiting to have argument order as the only way to carry information. It's particularly bad for repeating the same type many times:
sf::ContextSettings(0, 0, 0, 1, 1, sf::ContextSettings::Default, false); // wtf

I think opaque types are good, but not great. It makes sense to abstract to a certain level, but they are often overdone. Width/height is such an example -- both represent the same semantic type (a distance), just along different axes. What it implies is that you will need conversions for many basic operations (like comparing width and height), yet you can still not use them to represent other distances (such as the length of a vector). It's also notable that the width/height example wouldn't be a problem in the first place in the presence of named arguments.

A more practical approach I often saw is to separate similar, but semantically different types. A good example is to split the concepts of absolute state and relative modification. This can be applied to many domains, like:
  • Position + Direction (2D/3D space)
  • Offset and length in an array (1D)
  • Account balance and deposit/withdrawal
  • ...
Where it adds type safety is in operator overloading (subtracting two positions yields a direction) and being explicit about coordinate conversions. This is not an opaque type, because the two behave differently.



Now what can we do in C++?

For opaque types, the common workaround is just to define two separate types. Language abstraction mechanisms (templates) can help reduce code duplication. A not so good approach is typedef/using -- it may increase code clarity, but doesn't enforce type safety.

Unfortunately, it doesn't look like opaque types are a feature that can be emulated easily -- especially once we have methods, the only resort is to use the preprocessor or templates when defining the types. Duplicating an already existing type means copy&paste.

For named arguments, there are workarounds like builders/fluent setters:
sf::ContextSettings
    .antialiasingLevel(4)
    .depthBits(0)
    ...
or simply using the fields/setters directly:
sf::ContextSettings s;
s.antialiasingLevel = 4;
s.depthBits = 0;
...
The first approach is more flexible and allows immutable objects, but also very verbose to implement.

Regarding library support of named arguments, there are some interesting ideas, also from that blog https://www.fluentcpp.com/2018/12/14/named-arguments-cpp. Again, nothing new, just distilled what Boost.Parameters has been doing since 2005. The thing is, any solution that forces you to define your function as templates, is near-useless in practice, because those require definition in the header, exposition of implementation depencencies and largely increased compile times. So as nice as it looks as an academic exercise, a library like SFML couldn't switch to this implementation of named arguments.

It would be possible to get rid of templates using type erasure, but at other costs (type-safety only dynamically, extra indirection for parameter access). Any solution that allows for switching the argument order with compile-time checks must, by principle, be one of the following:
  • Overloads for all possible combinations (can be generated via preprocessor, lots of unnecessary declarations)
  • Accepting generic types and resolve proper order -> requires template
A decent trade-off could be to fix the order, but require (or allow) naming parameters. This would discard all the advantages of omitting default parameters, but could still be useful. Unfortunately, even here, there is no widely standardized idiom, so you would probably confuse a lot of users with such a new syntax, and also APIs may get more complex.

One important aspect of named arguments, which is often overlooked, is however: parameter names become part of the interface. While currently only existing as a documentation help, renaming a parameter with such a feature will introduce a breaking change to function callers.

Since C++ doesn't have native support for named arguments, I would suggest that things are either done the old-fashioned way (naming functions clearly, using enums instead of bool flags, offering fluent APIs), or when being "smart" with named argument emulations, make sure they are easy to implement, for both the library and the client. For SFML, I currently don't see the big use case apart from a few places like sf::ContextSettings, which are often instantiated using field assignments and not the constructor.
Zloxx II: action platformer
Thor Library: particle systems, animations, dot products, ...
SFML Game Development: