Do Haskell programmers not create mocks to test external components?
> OOP is the wrong-minded idea that a program should be a bundle of many "self-contained" objects. But that's wrong, we're writing ONE program here, not thousands.
The number of programs isn't the relevant metric. Complexity is. Any complex system is going to trend toward modularity. Modularity requires standard interfaces, which inevitably lead to bureaucracy.
A 1MM line Haskell program is going to be similarly bureaucratic. There are going to be standards you have to adhere to in order to play nice with the rest of the system. That's what typeclasses are, after all.
OOP is traditionally defined by three things: polymorphism, encapsulation, and inheritance.
Polymorphism: Modern Non-OOP languages can also be polymorphic, so that's no longer a differentiator.
Encapsulation: You definitely want encapsulation if your data is mutable.
Inheritance: This is the only truly problematic feature, and it's certainly abused, but it has its place. I don't always want to compose and delegate 20 methods when I just want to change the behavior of one.
> Polymorphism: Modern Non-OOP languages can also be polymorphic, so that's no longer a differentiator.
Haskell had ad-hoc polymorphism way before Java was a twinkle in its creator's eye. Before Haskell, Miranda (the language Haskell was based off of) could have kicked Java's polymorphism to the curb. Neither Java nor OOP invented polymorphism. If anything, they butchered it by introducing subtyping.
> Do Haskell programmers not create mocks to test external components?
The equivalent in Haskell would be having some kind of 'effects' system. An effect system differs from a mock object in that it limits in its totality what kind of interactions can take place. Typically, each layered effect also has a set of laws. Pure interpreters can be written for these effects, but the impure (i.e., real-world) interpreters are not privileged in their consumption of this effect. The pure interpreter also provides a proper implementation, such that you should be able to replace your real program with all pure interpreters, supply all your input at once, and still have a correct program. In other words, a Haskell program is typically polymorphic over which effects it uses in a way that other languages simply aren't.
> Encapsulation: You definitely want encapsulation if your data is mutable.
Again, Haskell, Miranda, and Lisp had encapsulation long before OOP came about, and Lisp has mutable data.
I think we're in violent agreement here. AFAICT, the feature sets of OOP and non-OOP languages have converged so much that inheritance is really the last differentiator. Maybe you could throw dynamic dispatch in there, but there's no reason in principle an OOP language couldn't add dynamic dispatch.
> I still find it hard to picture an immutable OO language.
Picture an object-oriented procedural language like Java, and then make everything immutable; for every 'void' method, return an updated copy of 'this'; for every non-void function, return a tuple of the updated copy and the value you were going to return. No change to const methods, obviously. And you're done, and you can still take advantage of encapsulation, polymorphism, and inheritance without any hoops. You can even do it in Java itself as a style thing without too much effort and only a modest amount of boilerplate. Alternatively, it's not that much work to build this same thing out of lambdas and dictionaries, if your language has those but not objects (adding mutation into that object system would then be trivial) (and of course, you can build dictionaries out of lambdas and lambdas out of objects, if need be).
> Do Haskell programmers not create mocks to test external components?
Do we create test substitutes, alternate implementations of the same interfaces? Yes. But dedicated mocking frameworks are crazy. In Haskell-like languages if you want an implementation of interface foo that returns bar when called with baz, you just... write an implementation of interface foo that returns bar when called with baz. If the easiest way to do that in your language is some kind of magical reflection-based framework, something is very wrong with your language.
> If the easiest way to do that in your language is some kind of magical reflection-based framework, something is very wrong with your language.
Languages have strengths and weaknesses. Certain tasks are easy in some languages, and certain other tasks are not. Throwing ones hands up and saying "something is very wrong with your language" because one is not familiar with a technique or tooling popular in another language is immature, IMO.
For example, if you explain how Debug.Trace works in Haskell to programmers familiar with Java, and they'd call it crazy.
> Languages have strengths and weaknesses. Certain tasks are easy in some languages, and certain other tasks are not.
Agreed, but we mustn't fall into the fallacy of assuming that means no language can ever be better or worse than another. There are good and bad language design choices, and "an implementation of interface foo that returns bar when called with baz" is not some obscure specialized feature, it's the basics of general-purpose programming.
> Throwing ones hands up and saying "something is very wrong with your language" because one is not familiar with a technique or tooling popular in another language is immature, IMO.
I'm very familiar with the techniques and tooling of mocking frameworks. I do not make these claims lightly.
> Agreed, but we mustn't fall into the fallacy of assuming that means no language can ever be better or worse than another. There are good and bad language design choices.
Agreed.
> "an implementation of interface foo that returns bar when called with baz" is not some obscure specialized feature
It isn't some obscure specialized feature in Java either.
Foo foo = new Foo() {
public Bar method(Baz baz) {
return new Bar("bar");
}
}
What mocking frameworks do is to provide a DSL to describe behavior of such implementations, use dynamic bytecode generation (not reflection, BTW) to create implementations of the interfaces dynamically, and bind them to simulate various test conditions. What the makes the language "worse" to require or allow doing this?
My Haskell is rusty, but given Haskell psuedocode like:
main :: IO ()
main = do
f <- foo
if (f == 1) then
putStrLn "Got 1"
else
putStrLn "Didn't get 1"
foo :: IO Int
-- ...
how would you test that the two branches of main behave appropriately?
This is not a snark; I am truly interested to know how Haskell gets rid of the need to bind alternate implementations of an interface for testing purposes.
instead of that, not because they need any of the mocking features as such but because it takes up fewer lines on the screen, particularly when there are more methods in the interface. (Partly a cultural problem of having overly large interfaces rather than a language problem per se, perhaps).
> use dynamic bytecode generation (not reflection, BTW)
How is it not reflection ("the ability of a computer program to examine, introspect, and modify its own structure and behavior at runtime")?
> What the makes the language "worse" to require or allow doing this?
Reflection or code generation means stepping outside the language and its usual guarantees - any time the programmer is forced to do it it's because the language didn't provide a good way to solve the problem within the language itself. It means you can no longer e.g. extract common expressions, because they don't necessarily mean the same thing; if you have some common mock setup code you can't just blindly automatically extract method, you have to think carefully about when the mocks get instantiated and when the expectations are set.
> how would you test that the two branches of main behave appropriately?
> This is not a snark; I am truly interested to know how Haskell gets rid of the need to bind alternate implementations of an interface for testing purposes.
It doesn't - as I said, you still write test implementations of your interfaces. What it does remove the need for is mocking frameworks, which people use in e.g. Java either because implementing the interface the normal way in the language is more effort (not a problem in Haskell), or because they want to test the specific interactions with the object (e.g. "verify that method foo was called twice") because those methods are used for side effects.
Haskell avoids that one by making it easier to represent actions as values; you can use e.g. a free monad to represent actions that will be performed later, so rather than testing that your complex business logic method called deleteUser(userId) on your mock, you instead test that it returns a DeleteUser(userId) value. To a certain extent you can do this in Java too ("the command pattern"), but without higher-kinded types you can't have a standard implementation of e.g. composed commands or standard methods for working with them, so it gets too cumbersome to really do in practice.
Even in Java you wouldn't want to use mocks for testing methods that operate on simple datatypes: to test e.g. a regex find method, you wouldn't pass in mock strings, you'd just pass in real strings and confirm that the results were true or false as expected. A language like Haskell just expands the space of what you can test in the same these-inputs-these-outputs way, by making it easier to represent more things as values.
> not because they need any of the mocking features as such but because it takes up fewer lines on the screen
Why is this a bad thing? How is this different from, say, using Template Haskell?
> It means you can no longer e.g. extract common expressions, because they don't necessarily mean the same thing; if you have some common mock setup code you can't just blindly automatically extract method, you have to think carefully about when the mocks get instantiated and when the expectations are set.
I have never found the existence of tests using mocks being a hindrance to refactoring in Java. Can you provide a more specific example?
PS:
> How is it not reflection ("the ability of a computer program to examine, introspect, and modify its own structure and behavior at runtime")?
Because it shows the language could be a lot better. A common, basic task shouldn't be so much easier outside the language (via reflection) than inside it.
> How is this different from, say, using Template Haskell?
It's the same thing. To the extent that people feel the need to use Template Haskell to do basic and common things, something is very wrong with Haskell.
> I have never found the existence of tests using mocks being a hindrance to refactoring in Java. Can you provide a more specific example?
I mean you can't refactor the test itself. Just basic things like: if you do expect(methodOne(matches("someComplexRegex"))) ; expect(methodTwo(matches("someComplexRegex"))), if you try to pull matches("someComplexRegex") out as a variable you'll break your test (you have to make it a function instead). You can't move an expect() above or below another method call without checking to see whether that was the method it was testing. Individually these things are trivial, but they add up to a chilling effect where people don't dare to improve mock-based tests a little as they work on them, so they end up as repetitive code with subtle variations, just like main code would if you never refactored it.
> Individually these things are trivial, but they add up to a chilling effect where people don't dare to improve mock-based tests a little as they work on them
In my experience, I have not come across such effects. People understand the purpose, strengths, weaknesses and limitations of the libraries they use and try not to "cut against the grain".
> people don't dare to improve mock-based tests a little as they work on them, so they end up as repetitive code with subtle variations, just like main code would if you never refactored it.
I understand this is a subjective preference, but I try not to refactor test code too much. I strive to make my test code not have branches ("if-less code" as some people call it). Sometimes this lead to slightly more verbose code, but in the long run I have found it useful for my test code to be rather boring.
----
I now understand the point you are making, and agree with it technically. I don't agree that those technical points lead to the social effect you call out, because I have not come across it.
Overall, Java makes two bad design choices - nullability by default, and mutability by default. But in the codebases I have worked with in the last few years I, and my colleagues, tend to not opt in to these defaults. This leads to pleasant, testable codebases to work with. We also enjoy acceptable performance, good tooling, easy-to-reason memory usage, great library ecosystem etc.
> Overall, Java makes two bad design choices - nullability by default, and mutability by default.
There are a few more, even today: using a weird secondary type system to track what kind of errors can occur (checked exceptions), classes being non-final by default, universal methods (every method in java.lang.Object except possibly getClass() ought to be moved to interfaces that user-defined types have the choice of not implementing), a bunch of syntactic ceremony around blocks (braces required everywhere, "return" being mandatory) which gets even worse once you want to move away from mutability by default, variance at use site only, no sum types, no HKT...
> This leads to pleasant, testable codebases to work with. We also enjoy acceptable performance, good tooling, easy-to-reason memory usage, great library ecosystem etc.
Sure. There are a lot of good things about the Java ecosystem, and if you see the language as a modest, incremental step over C++ then it is an improvement on that front at least. At the same time I do think ML-family languages - even ML itself - offer a lot of advantages especially if we're talking about them just as languages. In practice I work in Scala and gain most of the advantages of the Java ecosystem but with a language that has most of the advantages of Haskell as well.
Do Haskell programmers not create mocks to test external components?
> OOP is the wrong-minded idea that a program should be a bundle of many "self-contained" objects. But that's wrong, we're writing ONE program here, not thousands.
The number of programs isn't the relevant metric. Complexity is. Any complex system is going to trend toward modularity. Modularity requires standard interfaces, which inevitably lead to bureaucracy.
A 1MM line Haskell program is going to be similarly bureaucratic. There are going to be standards you have to adhere to in order to play nice with the rest of the system. That's what typeclasses are, after all.
OOP is traditionally defined by three things: polymorphism, encapsulation, and inheritance.
Polymorphism: Modern Non-OOP languages can also be polymorphic, so that's no longer a differentiator.
Encapsulation: You definitely want encapsulation if your data is mutable.
Inheritance: This is the only truly problematic feature, and it's certainly abused, but it has its place. I don't always want to compose and delegate 20 methods when I just want to change the behavior of one.