compareState does not really implement a proper ordering (STR > STL and STL >STR). Why not juststateEqual : State -> State -> Bool`?
STR and STL could be spelled out, they’re pretty obscure
validMoves seems poorly named, it searches for the whole solution
How about a dedicated type CharacterSet that implements the various operations you do on the List Character? You could start with type alias CharacterSet = List Character, that would already make the code more readable. (I’d consider giving up on the type Character and going for type alias CharacterSet = { goat : Bool, cabbage : Bool, wolf : Bool }.)
It is an example under my ai-search library. The problem space and the search strategy are defined separately, so you can change the underlying search. This level of flexibility is obviously not needed to solve the problem, so my solution is a bit longer than it might otherwise be, and I cannot really say it is a more elegant way of doing it.
I had a go at the 8-puzzle too, but didn’t finish that one.
I have also been thinking of writing a solver for the Rubix cube. I don’t know if there are search heuristics for that one.
Your last point is interesting - perhaps the state could have been implemented as a single record type (using perhaps a Left | Right type instead of a Bool)
The reason I did not model it this way, is that I wanted to be able to iterate the list of characters in order to try each of them out in turn. That seemed easier with a List Character.
I do find looking for the best data model in Elm can be challenging, I think because it is such an exacting and minimal language. Problems like this one are great for learning on, so keep 'em coming if you’ve got more.
P.S. I would also love to do an animation of this, with the characters getting in the boat and farmer rowing them accross. In Elm of course.