A question that often occupies the minds of language designers working with MPS is how to prevent users from making mistakes. There are two language aspects that deal with validation in MPS: constraints and typesystem. How to choose one over the other?
The constraints aspect lets the language designer restrict what concepts can appear in a certain place in the MPS node tree and thus prevent users from typing in incorrect code in the first place. The typesystem aspect lets the language designer write checking rules that are executed on existing code to check whether it is valid and display an error message if not.
Since one of the main reasons to use DSLs is to reduce mistakes, and the general software development wisdom says that it’s better to remove errors early in the process rather than fix them, one might conclude that constraints should be preferred over typesystem rules. Experience shows, however, that this is not the case.
It is better to let the users enter incorrect programs and check the programs for errors afterwards.
This may sound counterintuitive but there are several reasons. First, when you implement a constraint, MPS will preemptively remove entries that would violate the constraint from the completion menu. If constraints don’t allow ‘foo’ where you’re trying to type it in, you won’t be able to type it in, period. However, where there is nothing, there is also no explanation why. There is no space to let the user know that ‘foo is not allowed here because bar is set to baz’.
Second, your code that prevents ‘foo’ from appearing in the menu may be incorrect. You could have made a mistake or the requirements could have changed so that it’s now perfectly valid to use a foo even when bar is set to baz. The constraint now prevents the user from expressing what they wanted to express so they have to switch contexts: stop doing what they wanted to do and instead talk to you about removing the restriction, doing something else in the mean time. And you have to stop working on the new features and switch contexts as well to fix the bug.
If the error had been a typesystem error, the users could have continued with their work, temporarily ignoring or even suppressing the error, and you could have fixed the error a bit later.
But, you say, if we switch from constraints to the typesystem rules, doesn’t that violate the principle where it’s better to prevent errors rather than fix them? No, because we still show the error messages, so the users are still aware that they are doing something that is not correct according to the rules of our language. However, we give our users the power to override those warnings. We could still have our CI environment check that the code is correct and refuse to give the build the green light, for example.
So should you now go and rewrite all constraint rules as checking rules? No, there are still cases where constraint rules are perfectly appropriate. Here are some situations where I would write a constraint rule:
- Defining the scope of a reference. Be careful not to overly restrict the scope.
- Restricting concepts that are not generally applicable to their applicable context. For example,
it
is often used in various places to refer to something from the context. Often implemented as a special concept with acan be child
constraint that only allows it in an appropriate context. - Restricting child concepts of a concept. This can be done using a specialized link but you can only specialize to one
concept and sometimes you want to allow several different concepts in the child link. You can use a
can be parent
constraint in this case. This is quite a rare scenario though.
If you want to hear more tips about making your DSL and tooling more user-friendly and more agile, Kolja Dummann has given an excellent talk on this topic at the MPS Community Meetup last year.