6 ms·
This argument comes up against haskell over and over again and I think it is because it has some merit. Type level hackery and monad wizardry are great but it j
by greenlblue 16y ago
This argument comes up against haskell over and over again and I think it is because it has some merit. Type level hackery and monad wizardry are great but it just feels like a whole bunch of smart people showing off about how clever they can be with avoiding state and pushing it all into some high level categorical constructs.
- masklinn 16y ago> This argument comes up against haskell over and over again and I think it is because it has some merit. Yes and no. As with many other things, there's a tradeoff between Haskell's purity and referential transparency and side-effecting constructs such as storing stuff in a local cache, which is what memoization does. You can't litter printfs or cache calls throughout your pure functions either, "for some reason". It makes sense that's it's a pain to do in a (non-IO/state) haskell function, because those are very much about mixing side-effects into the code. A better solution would probably be to integrate memoization at the runtime level, with some compiler hints maybe. That way it becomes an implementation detail not an imperative side-effect, and the world is pure again. Anyway yeah, if you can't bear with Haskell's constraints you can just use something else, such as MLs (in order not to give up static type safety). Whoop the fucking do.
- klodolph 16y agoYou can litter printfs in your code, you'll just get a lot of hate if you do it in library code that you share. It's fine when debugging to write a "debug printf" function using "unsafePerformIO". Yes, it mixes side effects into pure code, but that's fine for debugging purposes. -- totally OK! go ahead! it won't crash or anything! debugPrint :: String -> a -> a debugPrint s x = unsafePerformIO (putStrLn s >> return x) You could also implement a memoization function using "unsafePerformIO". No, you won't get a visit from the FP purity police for doing so, it's perfectly fine. Why? Using "unsafePerformIO" in a function does NOT mean that that function is impure. It merely means that the compiler will not inform you whether the function is pure or impure -- the compiler's safety checks are bypassed when you use the function. Here's how I'd type the memoize function: memoize :: Ord a => (a -> b) -> IO (a -> b) Notice how it gives you a pure function out. No need to do stuff at the runtime level, or write compiler hints. It just works. If you want to memoize some nasty function: -- Totally fine! Unless nastyFunction is polymorphic... nastyFunction :: Paper -> PublishedBook nastyFunctionMemoized = unsafePerformIO (memoize nastyFunction) If you're new to Haskell, don't write memoize yourself if you want a version that works with polymorphic functions as that requires some more advanced techniques. Most people who need a memoized function don't need a polymorphic one though.
- masklinn 16y ago> You can litter printfs in your code, you'll just get a lot of hate if you do it in library code that you share. It's fine when debugging to write a "debug printf" function using "unsafePerformIO". Well obviously but I considered we were ignoring such "hacks" as unsafePerformIO. If you introduce it, then all of that is trivial to do so the discussion has no reason to be.
- klodolph 16y agoThe function "unsafePerformIO" is not a hack, it is the tool that allows you to write safe functions with the correct, pure type signature even though the compiler cannot prove that the function is safe. For example, suppose you write "memoize" without using "unsafePerformIO". Here's the resulting type signature: badMemoize :: Ord a => (a -> b) -> IO (a -> IO b) The resulting output has type "a -> IO b", but it doesn't actually do IO and it has no side effects! It's a normal, pure, referentially transparent function, and it shouldn't be in the IO Monad. The correct signature is "a -> b", just like the input. The "unsafePerformIO" function is a non-hack used by careful programmers to expose pure interfaces to code that uses impure constructs. See the FFI tutorials for more examples of using unsafePerformIO. (Of course, unsafe does mean unsafe... you can easily cause segfaults with that thing, you have to use it correctly.)
- masklinn 16y ago> The function "unsafePerformIO" is not a hack, it is the tool that allows you to write safe functions with the correct, pure type signature even though the compiler cannot prove that the function is safe. It is a hack, since it subverts the type system. It allows you to write safe functions with a pure (and potentially lying) type signature, but it also has no problem letting you create completely unsafe and broken functions. > The resulting output has type "a -> IO b", but it doesn't actually do IO and it has no side effects! Of course it does in its implementation, you do have to fill the memoization cache on a miss don't you? > The "unsafePerformIO" function is a non-hack used by careful programmers to expose pure interfaces to code that uses impure constructs. I don't agree with the first part of your declaration. At all. > Of course, unsafe does mean unsafe... you can easily cause segfaults with that thing, you have to use it correctly. And that's why it's a hack, just as using bit shifting instead of multiplication or division is a hack as it relies on the implementation details of the language and the hardware infrastructure. Yeah careful programmers can use it to wring out the latest drop of performance out of their software (assuming there is no optimizing compiler on the platform), but it's still a hack.
- yason 16y agoI've criticized Haskell many times, and being criticized many times of that. You see, I would love to love Haskell but in practice the language just always turns out to be an overly intellectual exercise. Real world programs need to manage state, preferably in a very controlled way, but the bottom line is that they need to do that in some obvious and easy way. It doesn't help if you explain me that you really don't need to explicitly store state anywhere, that you can just use monads to carry it over from computation to computation. Monads are an interesting abstraction per se but I don't want to use such heavy-weight artillery to make a mere assignment, to write a pointer to memory. I want my language to put some effort in protecting myself from the hazards of unintended concurrency but when I want to do it, the language must obey. That said, Clojure is just perfect. For now, Clojure is my Blub.
- klodolph 16y agoIt sounds like Haskell just isn't your style, but you're saying that Haskell is bad for "real-world" programming. And it sounds like you've never really used Haskell seriously (just a guess, most Haskellers I've met get over their monad fixation within a few months, yet you describe them as "artillery"). If you like Clojure so much, why not go champion that instead of complaining about Haskell? Besides, if you use "how easy is it to implement 'memoize'" as a benchmark then you'll always end up choosing a Lisp.
- swannodette 16y agoSupporting Haskell and complaining about Haskell are both valid activities. There's plenty to complain about Clojure. However, the post does point out quite succinctly that while Haskell features might be incredible for simplifying many kinds of problems, it's shocking how many simple problems become staggeringly complex in Haskell.
- plinkplonk 16y ago"while $Language features might be incredible for simplifying many kinds of problems, it's shocking how many simple problems become staggeringly complex in $Language." Replace "$Language" with almost any computer language in existence and that statement is true (given appropriately chosen values of "simple problems" and "staggeringly complex"). So it reduces to, "For any given language some things are simple and others complex. For other languages what is simple and complex differ". Not saying much, really.
- jrockway 16y agoThe description you give of "type-level hackery" and "monad wizardry" leads me to believe that you don't really understand what's going on. This is dangerous, because you are expecting these things to be complicated, but they are actually so simple that you've probably already invented them in your own computer programs. The Haskell community merely gives names to ideas you already know about. This isn't "smart people showing off about how clever they can be", it's people giving names to things that they use everyday. The problem people have with monads is that the word "Monad" does not describe something they're used to dealing with. A computer programmer is used to concrete things like lists, trees, functions, compilers; things you could hold in your hand (or at least draw on a whiteboard), poke at, and observe. Monads can't be understood in this way, because they are not anything concrete; "Monad" is a word to describe concrete things that, individually, are all very different. It's like explaining the concept of "Green" to someone by showing them an apple, a tennis ball, and some grass clippings. They will never understand "Green", because you can't understand what Green is by seeing three examples of them. Green is a color, not some sort of weird tennis-ball-apple-grass-clipping hybrid. Monads are the same way. List is a Monad. Reader is a Monad. State is a Monad. But when you look at these things and try to generalize, you might not see the parts that are the same, because the parts that are the same that make them all monads are hard to see. I think the hard part is that you hear about what defines a monad ("return" and "bind" or "return", "fmap", and "join"), and don't see how some dual or triple like that helps you write computer programs. Each set of functions is so different from the other sets that the fact that they are all called "return" and "bind" doesn't seem to matter. And that's exactly right; at some level, it doesn't matter. You use a List as a monad to combine computations that return multiple values, and you use Cont to write your programs in a continuation-passing style. Very different. Nothing similar at all, really. On the concrete level. The similarity does matter at a higher level, though; noticing similarities means you can write parts of your program in advance, and just tweak those parts for each specific case. An example is liftM2; it abstracts away the tedium of using a calling a two-arg function with monad-like semantics. If there were no similarities between List and Maybe, you'd have to write something like this: liftList2 f xs ys = concat $ (\x -> concat $ (\y -> [f x y]) `map` ys) `map` xs -- this doesn't compile; I want to use more descriptive words than the Data.Maybe API does :) liftMaybe2 f x y = if has x then extract x -> \x' if has y then extract y -> \y' makeMaybe f x' y' else Nothing else Nothing (Here's how to use the functions, in case you aren't sure: liftList2 (,) [1,2] [3,4] ==> [(1,3),(1,4),(2,3),(2,4)] liftList2 (,) [] [1..infinity] ==> [] liftMaybe2 (,) (Just 10) (Just 20) ==> Just (10, 20) liftMaybe2 (,) Nothing (Just "oh hai") ==> Nothing .) These seem very similar. There is the same sort of nesting in both structures, we seem to be saying the same thing twice in both functions. We seem to be applying functions to values "inside" each of these things. Maybe we can abstract those things away somehow, and make the two functions look a little more similar: newList x = [x] fmapList xs f = f `map` xs -- joinList xs = concat xs liftList2 f xs ys = joinList $ fmapList xs $ \x -> joinList $ fmapList ys $ \y -> newList $ f x y -- hey, the code looks nicer already! newMaybe x = Just x fmapMaybe Nothing _ = Nothing fmapMaybe (Just x) f = Just $ f x joinMaybe Nothing = Nothing joinMaybe (Just x) = x liftMaybe2 f x y = joinMaybe $ fmapMaybe x $ \x' -> joinMaybe $ fmapMaybe y $ \y' -> newMaybe $ f x' y' Now that we've given List and Maybe some common terminology, it's very hard not to see how these lift2 functions are nearly identical. We just need to do some refactoring, and then we can use the same definition for both! class Monad m where new :: a -> m a fmap :: m a -> (a -> b) -> m b join :: m (m a) -> m a instance Monad [] where -- haskell98 spells List as [] new = newList fmap = fmapList join = joinList instance Monad Maybe where new = newMaybe fmap = fmapMaybe join = joinMaybe liftM2 f x y = join $ fmap x $ \x' -> join $ fmap y $ \y' -> new $ f x' y' And there you go, that's what a Monad is. It's just refactoring, so we have less code to maintain! That's what any programmer would do, not just super-smart PhDs. You do the same thing every day!