Consider this example (from https://codereview.stackexchange.com/questions/23456/crtitique-my-haskell-function-capitalize):
import Data.Char
capWord [] = []
capWord (h:t) = toUpper h : map toLower t
capitalize = unwords . map capWord . words
Is there a good way to abstract over the "back and forth" transformation, e.g. unwords . f . words
? The best I could come up was
class Lift a b | a -> b where
up :: a -> b
down :: b -> a
instance Lift String [String] where
up = words
down = unwords
lifted :: (Lift a b) => (b -> b) -> a -> a
lifted f = down . f . up
capitalize = lifted (map capWord)
but it feels not very flexible, and needs MultiParamTypeClasses
, FunctionalDependencies
, TypeSynonymInstances
and FlexibleInstances
- which might be an indicator that it goes slightly over the top.
It's indeed not flexible enough! How would you lift a function to work on a line-by-line basis? You're going to need a
newtype
wrapper for that! Like soBut now there is no good reason to prefer the word-by-word version over the line-by-line one.
I would just use
unwords . map f . words
, to me that's the idiomatic "Apply f to all the words and put them back together". If you do this more often, consider writing a function.You could use a lens for this. Lenses are quite a lot more general than this I think, but anything where you have such bidirectional functions can be made into a lens.
For example, given
words
andunwords
, we can make aworded
lens:Then you can use it to apply a function inside the lens, e.g.
lifted f x
becomes(worded %~ f) x
. The only downside of lenses is that the library is extremely complicated, and has many obscure operators like%~
, even though the core idea of a lens is actually quite simple.EDIT: This is not an isomorphism
I had incorrectly assumed that
unwords . words
is equivalent to the identity function, and it is not, because extra spaces between words are lost, as correctly pointed out by several people.Instead, we could use a much more complicated lens, like the following, which does preserve the spacing between words. Although I think it's still not an isomorphism, this does at least mean that
x == (x & worded %~ id)
, I hope. It is on the other hand, not in the least a very nice way of doing things, and not very efficient. It is possible that an indexed lens of the words themselves (rather than a list of the words) may be better, although it permits fewer operations (I think, it's really hard to tell when lenses are involved).Like DarkOtter suggested, Edward Kmett's
lens
library has you covered, butLens
is too weak andIso
is slightly too strong sinceunwords . words
isn't an identity. You could try aPrism
instead.Now you can define
capitalize
asbut this has fairly pathological default behavior for your case. For
String
s which can't be mapped as isomorphisms (strings with multiple spaces in a row inside of them)over wordPrism g == id
. There ought to be an "over if possible" operator forPrism
s, but I don't know of one. You could define it though:Now, really, both of these are pretty unsatisfactory since what you really want is to capitalize all words and retain the spacing. For this
(words, unwords)
is too weak generally due to the non-existence of isomorphism that I've highlighted above. You'd have to write your own custom machinery which maintains spaces after which you'd have anIso
and could use DarkOtter's answer directly.I'd say the best answer is "no, because abstracting over that doesn't buy you anything". In fact your solution is far less flexible: there can be only one instance of
Lift String [String]
in scope and there are more ways to split string into a list of strings than justwords/unwords
(which means you'll start throwing newtypes or even more arcane extensions into the mix). Keep it simple — the originalcapitalize
is just fine the way it is.Or, if you really insist:
Conceptually the same thing as your typeclass, except without abusing typeclass machinery so much.
Your
lifted
is actually the same asdimap
fromData.Profunctor
:That might not be the direction of generalization you thought about. But look at the type of the equivalent function in
Control.Functor
fromcategory-extras
:This version generalizes it over everything which is both a
QFunctor
and a co-PFunctor
. Not that useful in everyday scenarios, but interesting.