User interface and user experience is a very complex topic. There are many things which influence how user perceivesS UI. Many small pieces which aren’t part of the core domain, but sometimes have huge impact at
final result. If everything is fine, then user even won’t notice that, but if something is wrong with this little piece then it will stand out like a sore thumb.
One example of that is correct noun flexion. Especially in language like Polish, because we have rather than complex flexion rules for nouns based on count. In order to provide proper user experience in application we should try to present content in correct grammar form.
Let see how we can use
F# to help us with that task.
To keep example simple, we will take a look on words in masculine. Yes, the rules are sometimes a bit more complicated ;) So, below in table, for one example is listed how it should be declined:
We can use feature from F# to name those rules and make easier for creating functions that will be using those rules. That feature is called
They are used to state frequent patterns that we use in pattern matching. But they also can act as a way for make complex patterns shorter. We can leverage active pattern feature to express our rules in following way:
let (|RuleForZeroAndFromFiveToNineAndTens|RuleForOne|RuleForTwoThreeAndFour|RuleFromElevenToNineteen|) n = match n with | 0 -> RuleForZeroAndFromFiveToNineAndTens | 1 -> RuleForOne | i when i >= 2 && i <= 4 -> RuleForTwoThreeAndFour | i when (i % 10 = 1 && i % 100 <> 11) -> RuleForZeroAndFromFiveToNineAndTens | i when (i % 10 = 2 && i % 100 <> 12) -> RuleForTwoThreeAndFour | i when (i % 10 = 3 && i % 100 <> 13) -> RuleForTwoThreeAndFour | i when (i % 10 = 4 && i % 100 <> 14) -> RuleForTwoThreeAndFour | i when i % 10 >=5 && i % 10 <=9 -> RuleForZeroAndFromFiveToNineAndTens | i when i > 10 && i < 20 -> RuleFromElevenToNineteen | i when i % 10 = 0 -> RuleForZeroAndFromFiveToNineAndTens
I could squash some rules together but I think there’s sense to leave it in such a way because it will be easier to maintain. It is simple as it looks, based on given count we compute which rule we should apply.
Having stated our rules as active pattern, we can easily create function which will help us to display proper form on the UI. Let’s see how it can be done:
let wordDeclination wordForZeroAndFromFiveToNineAndTens wordForOne wordForTwoThreeAndFour wordForElevenToNineteen n = match n with | RuleForZeroAndFromFiveToNineAndTens -> wordForZeroAndFromFiveToNineAndTens | RuleForOne -> wordForOne | RuleForTwoThreeAndFour -> wordForTwoThreeAndFour | RuleFromElevenToNineteen -> wordForElevenToNineteen
Thanks to active patterns we do not have to convolute method with logic that is responsible for selecting appropriate word flexion. Entire core logic is hiden in active pattern declaration.
wordDeclination we can further create small helper functions which will give a word in correct form. There is one important thing to be aware, here arguments order matters, because take a look if we will move
n argument at the beginning then we won’t be able to apply currying to
n argument. It should be clear if you look how we can use function
let forUIWydatekDeclination = wordDeclination "wydatków" "wydatek" "wydatki" "wydatków"
Here, we partially applied
wordDeclination function in
forUIWydatekDeclination function. As a result
forUIWydatekDeclination takes one argument and it is count of items. Looking again at function
wordDeclination, if we didn’t move the
n argument at the end, then we would have to provide also
n argument for
forUIWydatekDeclination function. We didn’t do that, so
n argument is curried. We can use
forUIWydatekDeclination in following way:
You may think that this is not rocket science, but as I said earlier those things influence overall user perception about application. Of course you could do that using different manner. But I think this is neat and clean use case for active patterns.