UI Test Automation Tools are Snake Oil 38
It happens over and over again. I visit a team and I ask about their testing situation. We talk about unit tests, exploratory testing, the works. Then, I ask about automated end-to-end testing and they point at a machine in the corner. That poor machine has an installation of some highly-priced per seat testing tool (or an open source one, it doesn’t matter), and the chair in front of it is empty. We walk over, sweep the dust away from the keyboard, and load up the tool. Then, we glance through their set of test scripts and try to run them. The system falls over a couple of times and then they give me that sheepish grin and say “we tried.” I say, “don’t worry, everyone does.”
It’s a very familiar ‘rabbit hole’ in the industry. It’s sort of like the old days when you’d find a couple of classes generated by a CASE tool in every code base you visited if you looked hard enough. People started with the merry notion that they were going to round-trip code with some CASE tool and they learned (like most lucky teams do) that it just doesn’t pay for itself, it’s not worth the time or the frustration. UI Test Automation tools are in the same category. Personally, I think that in this day and age selling them is irresponsible. Developing them open-source? Well, let your conscience be your guide, but really, even though people can use them responsibly, they hardly ever do because these tools are sold with a dream, a very seductive and dangerous one.
The Dream
Janet comes into work in the morning and she sits down at her super-duper testing console. She presses a button and the testing system springs to life. The application comes up all at once across ten monitors. Cursors move, selections are made (silently) and tests run against the user interface magically, as if some eager set of ghost elves took control, mischievously burrowing through the nooks and crannies of the application, running scripts to completion, and making little notes whenever there is a failure. Janet sits back in her chair, waiting for the elves to report back to her. She stirs her coffee gently.
The Reality
Janet hasn’t gone home yet. It’s 2AM and she has to report completion of all her test cases at a meeting in the morning. She thinks she’s past the last configuration issue but she’s not sure. For the last hour, she’s been trying to make sure that a particular button is pressed at step 14 of her script, but quirky latency on the server is preventing it from happening consistently. Sadly, she has to run the script from the beginning each time. Oh, and five hours ago she discovered UI changes which invalidated 30% of the regression tests. Most of the changes were easy but she still has 12 cases to go and her 9AM meeting looms ahead of her.
This gap between the dream and the reality is not a matter of flawed execution, it’s endemic. Here’s the scoop.
UI Test Automation Tools are Brittle
You might not think this is fair but it is, really. I haven’t seen one of these tools yet which isn’t susceptible to missed events or timing madness. It just happens. The fact of the matter is, it is hard to sit on the outside of an application an instrument it. It’s a very technology sensitive problem. You need to hook into either the OS or the browser or both. Neither are ever really built from the ground up for that sort of access.
UI Based Testing Is Not the Solution That Vendors Imply It Is
This is the big issue, the one which really hurts the industry. The fact of the matter is that UI based testing should be used for UIs: that’s it. You should not be testing your full application end-to-end through a UI Testing tool. First of all, that sort of testing couples some of the most important tests in your system to one of the most volatile parts of it. It’s easy for us to see business logic as volatile, but really, the UI is the thing which twists and ripples in the winds of change. When customers want new features, often those features involve new workflows. When usability experts discover better ways of models of interaction, an agile business seizes upon them and makes the changes—if they can. You’d be surprised at the number of applications which continue to sport out of day user interfaces simply because the development organization is terrified of throwing away all of their regression tests which (by the way) go through the UI. Even if you’re not a consultant like me, visiting teams and seeing their development processes, you can see hints from the outside. Think of every website or shrink-wrap application which has “the same old workflow” and a UI that has become more cluttered over the years. Often it’s because of that lock-in.
UI Based Testing Takes More Staff and Time Than You Expect
This, really, is the most common failure case. It’s the case which explains the dust on the testing box’s keyboard. Someone, usually disconnected from the development organization, decides that “hey, we need to solve the testing problem. We have too many people doing manual testing. It’s taking forever.” So, they do their research, find a vendor with with a good licensing model and a good pitch and then they push it on the development organization. They are, of course, looking to reduce staff so when they realize that translating all of those manual tests to the tool is very labor-intensive, they are taken aback. But, of course, it is just a temporary cost, right? But, then it takes far longer than they expect. Remember Janet’s story? It’s really hard to catch up with a UI-Based testing tool. It’s hard to even stay in place with one. Typically it takes a number of people to do UI-Based automated testing for a development team in sync with an iteration and worse, they’ll always lag behind a bit because you can’t really write UI-based tests ahead of time the way you can with FIT and other beneath-the-UI testing tools. From what I’ve seen UI-based testing, done diligently, takes the effort of about one tester for every two to three developers. That’s what it seems to cost amortized across all of the maintenance of UI-induced test breakage. Oh, and by the way, if think you are going to save labor using record and playback? Nope, you aren’t. It doesn’t work.
Solutions
The fact of the matter is, you can use these tools effectively, but in a very narrow space. It’s nice to be able to test the UI—by itself. However, this sort of thing requires an architectural change.
In general, UIs are too volatile for end-to-end testing. Teams that do it well, typically develop a small task-focused scripting layer and build tests on top if it so that the actual tests don’t touch the UI directly. But, if they happen across that technique, they are lucky. Still, it isn’t an ideal solution. You really want to be below the UI working against an API which exposes the business logic. And, because of that nearly mystical synergy between testability and good design, that API layer is often useful for many things other than testing.
Conclusion/Challenge
I recognize that I’ve been rather vicious in the this blog. If you develop these tools for a living, you might not think it’s fair. But consider this. If you don’t think I’m being fair, take a look at how your tools are marketed. In particular, show me where the product literature discourages end-to-end testing through the tool. Otherwise, well, you know, you are probably developing snake-oil.
vih 18
Over the past few days, I’ve been tinkering with a little project I’ve called vih – it’s a vi clone written in Haskell. Here’s the git repo.
The way I’ve started it is rather naive, and frankly I’m surprised I that I had gotten as far as I did without curses and with the simple data structure I choose. Rather than using a clever representation for a buffer, I decided to just use a String. What this means is that every time a character is inserted into a buffer, the whole thing is split and reconstituted. I’m not using Data.ByteString or Data.ByteString.Lazy either. I suppose that I’ll move toward a more sensible data structure in a while but right now typing is comfortable even with all of that work going on.
My intention in doing this was to start to get a sense of how data structures evolve in Haskell by growing something large from a small seed. The primary data structure that I’m using right now is called EditBuffer and I notice that quite a few of my functions translate an EditBuffer to another EditBuffer. This seems to work fine, however I find that I almost always have to label the buffer in the argument list and deconstruct it. I haven’t seen pervasive use of labels like that in Haskell code I’ve looked at, so I’m wondering if my mapping of functions to data structures is odd.
Right now, vih supports insert and command mode (toggled with ‘i’ and ‘ESC’), the h, j, k, l cursor movements, line deletion with ‘dd’, home with ‘gg’, end of file with ‘G’, `in line’ positioning with ‘0’ and ’$’, insert new line after with ‘o’, and delete current char with ‘x’. No file I/O yet.
Have a look, fork or contribute.
Over the next month.. 3
I’m off on a round of conferences, classes and talks soon, so I figured I’d list them here.
In about 2 weeks, I’ll be at Agile2009. While there, I’ll be doing a workshop called Treating Errorhandling as a First Class Consideration in Design. This might be as much tutorial as workshop. I’ve been collecting a lot of material on error handling over the past few years and it will be nice to present some of it.
I’ll also be doing a talk called Test Driven Development: Ten Years Later with Steve Freeman on the main stage. We gave this talk at QCon London last year. It’s chock-full of history and reminiscences.
Some people in the Software Craftsmanship community pulled a sneaky and organized Software Craftsmanship North America (SCNA), a one-day conference during Agile2009 across the street. I’ll be speaking there as well.
Later in the week at Agile, I’ll be doing a workshop with Naresh Jain called Styles of TDD: First Test. Naresh has had a long-standing interest in the different ways that different people approach TDD, and we hope to throw the spotlight on that aspect in the workshop. Brett Schuchert and I will also be doing something terrifying with legacy code in public early one morning during the conference. Details as it finalizes.
It should come as no surprise to readers of this blog that I’m interested in functional programming. I’ve had a casual interest for a while, but recently, I’ve decided to go full bore and dedicate whatever spare time I have to it – to the exclusion of the other design and programming topics I spend time investigating/thinking about. So, the week after Agile, I’m going to ICFP and its companion practitioner conferences DEFUN2009 and CUFP. I’m looking forward to meeting some of the people who’ve been helping me on and off and chatting with me on irc and mailing lists.
A week or so after that, I’ll be teaching a Three-day TDD/Refactoring course in Stockholm. There are still sign ups available. I’m looking forward to that trip as well.
Looking out past September, I’ll be speaking at JAOO in Aarhus, and Agile Vancouver. More about those later as the talk times and topics finalize.
Naming and Body Language in Functional Code 21
I wrote a blog the other day about functional refactoring and I had what I thought was a good example:
absPosition :: Buffer -> Int
absPosition (Buffer (x, y) contents) = x + lenPreviousLines contents
where lenPreviousLines =
foldr ((+).length) 0 . take y . terminatedLines
Almost immediately, I saw replies on a couple of forums (including this one) which pointed out that I could’ve written the code this way:
absPosition (Buffer (x, y) contents) = x + lenPreviousLines contents
where lenPreviousLines =
sum . map length . take y . terminatedLines
It’s funny, I thought of using sum instead of foldr back when I was using Haskell’s line function. The code I had looked like this:
absPosition (Buffer (x, y) contents) = x + lenPreviousLines contents
where lenPreviousLines =
foldr ((+).((+1).length)) 0 . take y . lines
But, I realized that the code wasn’t in great shape for sum, so I created terminatedLines, used it and promptly forgot to do the refactoring I set out to do.
terminatedLines :: String -> [String]
terminatedLines = map (++ "\n") . lines
From an imperative point of view, terminatedLines looks a bit silly: What?? You’re going to append a newline to each line in a list of lines you just created just so that you can count it?? But, I suspect
that it isn’t that bad. The evaluator pulls values from each line and as it reaches the end of one it should just put a newline at the end of it. If I’m wrong about this, please let me know.
In any case, I agree that the code looks better with sum that it does with foldr (+) 0. The big question is – should we refactor any more?
Someone with the handle sterl suggested a very cool trick. I could drop the where clause like this:
absPosition (Buffer (x, y) contents) =
x + (sum . map length . take y . terminatedLines) contents
And then move on to this:
absPosition (Buffer (x, y) contents) =
sum . (x:) . map length . take y . terminatedLines $ contents
What’s going on here? Well as sterl put it, we’re summing anyway so why not prepend the x onto the list that we are already summing?
Part of me likes this and part of me doesn’t. One the one hand, it’s brief, but on the other hand, the code isn’t telling us why it is doing what it is doing anymore. In the original code, there is an algorithm:
To get the absolute position, add the x position of the location to the sum of the lengths of all of the previous lines.
In the new code, the algorithm is:
To get the absolute position, sum the current x position with the lengths of all of the previous lines.
Wait, that’s sort of the same, isn’t it?
This example points to a fundamental dilemma that I have with naming in Haskell. I’m used to introducing names in lower-level languages to bridge the gap between intention and mechanism, but what happens when your mechanism is so high-level that it can speak for itself? Maybe we don’t need names as much?
Now, I know as I write this that someone is going to look at this as an extreme statement. It isn’t. Names are useful, and indispensable, but really they are only one of several ways of communicating meaning. In each case, we have to pick the right tool for the job. With Haskell, I think that programmers communicate with structure as much as they communicate with names. It’s the body-language of their code.
Imposing the Edges Later 12
Here’s what wikipedia has to say about lazy evaluation:
The benefits of lazy evaluation include: performance increases due to avoiding unnecessary calculations, avoiding error conditions in the evaluation of compound expressions, the ability to construct infinite data structures, and the ability to define control structures as regular functions rather than built-in primitives.
It’s all true, but I think it misses a point. One of the nicest things about lazy evaluation is that it enables a different form of abstraction. Take a look at this code. It computes the Mandelbrot Set. There are many ways to code up this algorithm, but the code I just linked to is very typical – it’s a doubly nested loop which iterates over a specific rectangle in the complex plane. It’s hard to see anything that’s awkward about its iteration strategy in a procedural language – the algorithm does require starting and ending points in the plane.
Let’s look at a different way of approaching this. Here’s part of a Mandelbrot Set generator in Haskell:
mandelbrot :: Double -> [[Int]]
mandelbrot incrementSize =
[[ escapeIterations $ translate offset . translate (x,y) . mandel
| x <- increments]
| y <- increments]
where increments = [0.0, incrementSize .. ]
window :: (Int, Int) -> (Int, Int) -> [[a]] -> [[a]]
window (x0, x1) (y0, y1) = range y0 y1 . map (range x0 x1)
where range m n = take (n-m) . drop m
The interesting bit is the mandelbrot function. It contains a doubly-nested list comprehension which, essentially, mimics the behavior of a doubly-nested loop in the procedural algorithm. However, there is one interesting difference. This list comprehension actually represents the mandelbrot computation across a full quarter of the infinite plane – it goes from zero to infinity in x and y. All we have to do is pass it an increment size and it is configured to create that grid. If we want to zoom in to a particular place in the Mandelbrot Set, we can use the window function like this:
-- compute the mandelbrot from (10,10) inclusive to
-- (20,20) exclusive with an increment of 0.05
window (10, 20) (10, 20) $ mandelbrot 0.05
The trick to this code is in window’s range computation. It takes a starting position, an ending position, and a list and it drops all of the elements of the list before the starting position, without evaluating them, and then it takes, from the front of the new list, end - start elements, giving us a sub-range from the original list. When that elements of that sub-list are evaluated, the computation for each point in the window occurs.
This code yields the computation that we want and the neat thing is, we are really only computing the piece we specified with window. We’ve essentially formed a general computation and imposed the edges later.
Looking back now at the imperative mandelbrot code, it’s easy to see that it was, in a way, mixing two concerns. The code which determined the range was mixed with the code which generated the set. Laziness allows us to separate the two concerns.
Note: There are much more direct ways of computing the Mandelbrot Set in Haskell. Here’s one which is particularly brief.
Functional Refactoring and "You Can't Get There From Here" 17
I’ve been working in Haskell recently, and I find myself doing much less refactoring. In fact, I rewrite more than I refactor.
I’m not talking about massive refactoring, but rather the sort of micro-refactoring that I do to move from one algorithm to another. If you are working in an imperative language, you can often make micro waypoints – little testable steps along the way as you change your code from one shape to another. You can add a variable, confident that behavior won’t change because you haven’t used it yet, and you can move one statement down below another, changing the order of a calculation if (again) you are confident that the overall computation is order in variant. As I TDD my code, I do those sorts of things. I often refactor by introducing a parallel computation in the same method as the old one, and if the test passes, I knock out the old way of doing things and leave the shiny new one.
In Haskell, however, I don’t do that, and it reduces the amount of time that I spend refactoring. Yes, I rename variables, add parameters, and introduce where-clauses, etc. But, refactoring in Haskell seems different, and I think I know why now. There are two reasons and they sort of intermingle.
Imperative programs are built of little state changing variables, and one of the effects is that you can have lots of little bits changing independently. It’s rather easy to add mutating state variables and have “several calculations in the air” in a method while you are along the way toward reducing it to one. In functional programming, however, you use bigger pieces. Instead of looping, you use map or fold – you end up with terse code which packs a lot of punch.
Here’s a function which determines a position within a document from a column/row coordinate (affectionately named x and y here):
absPosition :: Buffer -> Int
absPosition (Buffer (x, y) contents) = x + lenPreviousLines contents
where lenPreviousLines = foldr ((+).length) 0 . take y . terminatedLines
The thing to get from this example if you don’t read Haskell is that it is fully compositional. Each of the dots in the definition of lenPreviousLines is a composition operation. We apply the terminatedLines function to a string, and take its output and pass it to the function ‘take y’, then we pass its output to ‘foldr ((+).length) 0’. The net effect (take my word for it) is that we end up with the sum of the lengths of a set of text lines.
That small piece of code performs a rather large chunk of work. It takes a string, splits it into lines, appends a newline to each line, takes y of the lines and then sums the count of their characters. But, to me, that’s not the interesting part. The interesting part is that it is tight. There is no space for a parallel computation, no place where you can perform some computation off to the side and progressively move toward a different algorithm. If you want to change the algorithm you essentially rewrite it – you replace some chunk of that code with another chunk of code.
One of the beautiful things about pure functional programming is that it eliminates side effects. That, by itself, might not impress you, but the thing that it enables is very powerful, when you remove side effects you lose all barriers to composability. It’s easy to make little tinker-toy-like parts which you piece together to do your work, and, in fact, that’s exactly what has happened in Haskell. Most of the time, you can find a little function which will do what you want. You don’t often have to drop down into primitive list operations or recursion. Instead, you have this substrate of little composable bits. We don’t have anything like that in traditional languages. We drop down to loops, assignment and mutable variables all the time.
The end result, I think, is that a lot of refactoring in Haskell is more like rewriting, or (to be precise) it is more like the ‘Substitute Algorithm’ refactoring in Martin Fowler’s Refactoring book. In pure code, there isn’t any extra bandwidth for building up parallel computations. The pieces are bigger, so many refactorings are more like leaps than micro-adjustments toward a goal.
Is this good or bad? I don’t know. I think it is just different. Personally, I’m happy to have higher level pieces, although, I have to admit, I sometimes miss the fluidity of work with more granular primitives. It’s a place you can always drop down to and build back up from. In Haskell, at the micro-level, you have to fix your sights on a new target and rebuild.
Static Typing vs. Dynamic Typing 12
Some sculptors prefer clay and some prefer marble. Some choose on a piece by piece basis, depending upon what the work demands. The important thing is to respect the medium and understand its affordances and limitations.
Ending the Era of Patronizing Language Design 40
Earlier this year, I was asked to speak at a Ruby Conference. I was happy to go, but I also felt a bit out of place. I haven’t done much Ruby, but I’ve admired the language from afar. I have a number of friends who’ve left C++ and Java to jump toward Ruby and Python and for the most part, they are happy. They do great work, and they enjoy it. They are living proof that the nightmare scenarios that people imagine about dynamic languages aren’t inevitable. You can program safe, secure, high quality applications in dynamically typed languages. People do it all the time, but that’s cultural knowledge. If you are in a culture, you hear about all of the things which are normal which appear odd from outside. If you aren’t, you don’t.
This is pretty much the situation I’ve been in with Ruby, up to a point. I haven’t written a large Ruby application. I’ve tinkered around with the language and written utilities but as far as total immersion goes — no, I’ve never been totally immersed in the language but that hasn’t kept me from learning noticing interesting things at the edge. One of the striking things that I’ve noticed is that the attitude of Rubyists toward their language is a bit different. They seem to have an ethic of responsibility that I don’t see in many other language cultures.
Ethic of responsibility? What do I mean by that?
I guess I can explain it this way. In many language communities, people are very concerned with the “right way” to do things. They learn all of the warts and edges of the language and they anticipate the ways that features could be misused. Then, they starting writing advice and style guides — all the literature which tells you how to avoid problems in that language. The advice goes on and on. Much of it centers around legitimate language defects. Some languages make you work hard to use them well. Other bits of advice, though, are really extensions of culture. If a language gives you mechanisms to enforce design constraints, it doesn’t feel quite right to not use them. As an example, consider sealed and final in C# and Java. Both of those constructs do pretty much the same thing and people do go out of their way to advise people on how they should be used to protect abstractions. It’s interesting, however, that languages like Ruby, Python, and Groovy don’t have anything similar, yet people do write solid code in those languages.
Let’s leave aside, for a minute, the debate over static and dynamic typing. What I think is more important is the issue of tone. In some languages you get the sense that the language designer is telling you that some things are very dangerous, so dangerous that we should prohibit them and have tools available to prohibit misuse of those features. As a result, the entire community spends a lot of time on prescriptive advice and workarounds. And, if the language doesn’t provide all of the features needed to lock things down in the way people are accustomed to, they become irate.
I just haven’t noticed this in Ruby culture.
In Ruby, you can do nearly anything imaginable. You can change any class in the library, weave in aspect-y behavior and do absolutely insane things with meta-programming. In other words, it’s all on you. You are responsible. If something goes wrong you have only yourself to blame. You can’t blame the language designer for not adding a feature because you can do it yourself, and you can’t blame him for getting in your way because he didn’t.
So, why aren’t more people crashing and burning? I think there is a very good reason. When you can do anything, you have to become more responsible. You own your fate. And, that, I think, is a situation which promotes responsibility.
For years, in the software industry, we’ve made the assumption that some language features are just too powerful for the typical developer — too prone to misuse. C++ avoided reflection and Java/C# avoided multiple-inheritance. In each case, however, we’ve discovered that the workarounds that programmers apply when they legitimately need a missing feature are worse than what the omission was meant to solve. Blocks and closures are good immediate example. There are tens of thousands of applications in the world today which contain duplication that you can really only remove with the template method design pattern or by creating a tiny class which encapsulates the variation. If blocks or closures were available, programmers would be more likely to tackle the duplication and arrive at much less cluttered design.
Meta-programming features are yet another example. Business applications are rife with situations where you need to know the value, type, and name of a piece of data, yet we use languages in which these sorts of capabilities have to be hand-coded and over and over again. The fact that it took decades for the industry to arrive at something as useful as ActiveRecord in Rails is due primarily to the attitude that some language features are just too powerful for everyone to use.
We’ve paid a price for that attitude. Fortunately, I think we are getting past it. The newer breed of languages puts responsibility back on the developer. But, language designers do persist in this sort of reasoning — this notion that some things should not be permitted. If you want to see an example of this style of reasoning see the metaprogramming section in this blog of Bruce Eckel’s ( http://www.artima.com/weblogs/viewpost.jsp?thread=260578 ). I respect Bruce, and I realize he isn’t speaking as a language designer, but I offer that as a example of that type of reasoning — reasoning about what should be permitted in a language rather than what puts a bit more control and responsibility in the hands of programmers. Maybe decorators work in 95% of the cases where you would want to do metaprogramming in an application, but there is a price to that choice, and it isn’t just the workarounds in 5% of the cases. The additional price is a decreased sense of responsibility and ownership. I think that those human dimensions have far more impact on software than many people suspect.
The fact of the matter is this: it is possible to create a mess in every language. Language designers can’t prevent it. All they can do is determine which types of messes are possible and how hard they will be to create. But, at the moment that they make those decisions, they are far removed from the specifics of an application. Programmers aren’t. They can be responsible. Ultimately, no one else can.
X Tests are not X Tests 14
Testing is a slippery subject, and it’s reasonably hard to talk about for one simple reason: the nomenclature is chaotic. Years ago, I went to a summit with some testing gurus. I was one of the lone developers there and I asked about the taxonomy of testing. Cem Kaner, Bret Pettichord, Brian Marick, and James Bach went through it for us on a flipchart, and it was a nightmare. You can name tests after their scope: (unit, component, system), their place in the development process (smoke, integration, acceptance, regression), their focus (performance, functional) their visibility (white box, black box), the role of the people writing them (developer, customer).. The list goes on. There are far more than I can remember.
Why is it so confusing? There are are a couple of reasons. One is that different communities have developed different nomenclature over time. But, let’s face it, that’s true in most fields. The thing which makes testing nomenclature worse is that the tests themselves aren’t all that different, or at least, they are often not different enough for us to for us to distinguish them without being told. Yes, we can tell the difference between a unit test and an acceptance test in most systems, but really there is no force which prevents tests of different types from bleeding through into each other. Often the “type” of a test is more like an attribute: “here I have a blackbox smoke test, written by a developer for component integration.” In the end, all we have are tests and each of them can serve purposes beyond the purpose we originally intended.
Earlier today, I read a blog by Stephen Walther: TDD Tests are not Unit Tests. In it, he draws some distinctions between various types of testing. It’s great that he wrote it because it’s nice for us to have mental categories for these things, but we have to remember is that they really are just categories. We get to choose how distinct they will be. When I write code, most of my TDD tests end up being the same as my unit tests. I find value in forcing that overlap, and in general, I think overlapping test purposes are great to the degree that the purposes don’t conflict. You get more for less that way.
I don’t see any remedy for the muddle of test types. We will continue to make up terms to distinguish tests. We’ll just have to remember that the types are labels, not bins.
A Brief Collection of Convenient Lies about Functional Programming 6
- A value is the instantaneous state of an object. – In OO languages, we have objects. In FP languages, we throw out the object and instead manipulate the values it would take on over time.
- Algebraic data types are classes. – Every case in an ADT is a state that an “object” can be in.
When we write functions over ADTs, we are obliged to cover all of the cases. So, for instance, if we define depth for Empty, we have to define depth for the Leaf and Node cases as well. When we do, we can evaluate depth t for any tree value and have a well-defined result.data Tree = Empty | Leaf Int | Node Tree Tree - The functions which we define over an ADT can be considered its public interface. – There’s a school of thought which says that encapsulation doesn’t matter in an functional programming language because values are immutable and corruption can’t happen. Nothing could be further from the truth. If we add or remove a case from an ADT all of the functions which pattern match against it are impacted. While we don’t need to have an encapsulation boundary as tight as we might have in an OO language – it pays to be conscious of how far ADTs travel in a program. Encapsulation is the act of forming a boundary by transforming an ADT into some other form of data.
Each of these statements is a lie, an artful simplification, but they are a convenient and not entirely false way of thinking about functional programming until it becomes second-nature.
