Often, as well-meaning programming language enthusiasts, we might find ourselves encouraging newcomers with simplified descriptions of language features. There’s sometimes a tricky balance between explaining things in an approachable way and explaining them correctly and precisely. Let me give some examples of what I’m talking about, and then we can zoom out to think about the implications.
Ruby
Ruby, like many OO languages, has visibility methods that let you ensure nobody will depend on the code you’re using! This helps you know what’s safe to modify!
Sure enough, the basics work like we'd expect:
class ActivateUser
def call(activation_code)
user = fetch_user(activation_code)
raise ArgumentError, "Activation code '#{activation_code}' is invalid" if !user
user.update!(:activation_code => nil, :active => true)
user
end
private
def fetch_user(activation_code)
User.where(:activation_code => activation_code).first
end
end
class TestActivateUser < Minitest::Test
def setup
User.delete_all
User.create(:email => "jen@example.com", :activation_code => "GOOD_CODE")
@activator = ActivateUser.new
end
def test_a_bad_activation_code_cant_be_fetched
assert_raises(ArgumentError) { @activator.call("BAD_CODE") }
end
def test_a_good_activation_code_updates_and_returns_the_user
user = @activator.call("GOOD_CODE")
assert_equal user, User.where(:email => user.email).first
assert_equal "jen@example.com", user.email
assert_nil user.activation_code
assert_equal true, user.active
end
def test_a_good_activation_code_updates_the_user
user = @activator.call("GOOD_CODE")
assert_equal "jen@example.com", user.email
assert_nil user.activation_code
assert_equal true, user.active
end
def test_private_method_access_prevented
assert_raises(NoMethodError, /private method `fetch_user` called for/) {
@activator.fetch_user("GOOD_CODE")
}
end
end
But we can bypass private methods in at least a few ways:
def test_private_method_access_allowed_with_send
user = @activator.send(:fetch_user, "GOOD_CODE")
assert_equal "jen@example.com", user.email
end
def test_private_methods_can_be_made_public
# We could send this message to `ActivateUser` instead, but then we'd
# need `Minitest::Test.i_suck_and_my_tests_are_order_dependent!` to keep
# our tests deterministic, and nobody wants that.
@activator.singleton_class.send(:public, :fetch_user)
user = @activator.fetch_user("GOOD_CODE")
assert_equal "jen@example.com", user.email
end
Ah, Ruby. private
is a principle, not a guarantee.
Clojure
Use Clojure - you don’t have to worry about anyone mutating your collections out from under you! Immutability by default!
Not so fast. This test suite passes too:
(test/deftest test-immutability
(test/testing "the awesomeness of immutability"
(let [xs [0 1 2 3 4 5 6 7 8 9]
ys (map (fn [x] (* x x)) xs)]
(test/is (= (range 10) xs))))
(test/testing "the pleasing nature of... wait what??"
(let [xs [0 1 2 3 4 5 6 7 8 9]]
(do-something xs)
(test/is (= (repeat 10 :boom) xs)))))
In Clojure, you can use Java interop to mutate PersistentVector. Similar things are possible for other persistent data structures.
(defn- replace-array [f arr]
(dotimes [i (alength arr)]
(let [current (aget arr i)]
(aset arr i (f current)))))
(defn- replace-node [f root]
(let [node-arr (->> root .array)
first-node (first node-arr)]
(if (and first-node (instance? clojure.lang.PersistentVector$Node first-node))
(doseq [node (filter identity node-arr)]
(replace-node f node))
(replace-array f node-arr))))
(defn mutate-vector [f xs]
(replace-node f (.root xs))
(replace-array f (.tail xs)))
(defn do-something [xs]
(mutate-vector (constantly :boom) xs))
Ah, Clojure. "Immutability" is a principle, not a guarantee.
Haskell
You should use Haskell, where the type system enforces immutability and even side effects! You can tell just by looking at the type signature!
You can probably sense where this is going: this isn’t really true either. unsafePerformIO
can take you to some pretty gnarly places:
count :: String -> Int
-- definition redacted for now
data StringData = StringData {
text :: String,
reversed :: String,
size :: Int
} deriving (Show)
stats :: String -> StringData
stats text = StringData {
text = text,
reversed = reverse text,
size = count text
}
main :: IO ()
main = print (size (stats "Hello, World!"))
Since count
returns an Int
, we should see main
printing an Int
. But that's not what happens!
It prints:
"OHH NOOOOOO"
13
Despite the type signature clearly indicating purity, we’re able to do whatever impure side-effecting operations we want:
import System.IO.Unsafe (unsafePerformIO)
count :: String -> Int
count text =
unsafePerformIO $ do
print "OHH NOOOOOO"
return $ length text
Ah, Haskell. Purity is a principle, not a guarantee.
Zooming out
If you're a developer and/or advocate for one of these technologies, I’m sure you have your counter-arguments ready about why the example in your preferred language isn’t such a big deal. It might surprise you, but I don’t disagree with the counterpoints—far from it!
My point is not that everything is on fire and nothing is sacred. Affordances to help developers do the right things can still be very useful, even when they’re not guarantees. You can break a law, but laws are still useful. And programming principles with escape hatches can be, too.
I don't want to litigate the necessity of these escape hatches for the language designers, because I don't think it's actually that important. Whatever the reasons, these facilities give us the power to do things that seem to conflict with key principles in these languages.
And when we overstate our opinions and frame principles as guarantees, to put it charitably we confuse people about what they can and should believe. We don’t have to give everyone all the details of edge cases like these. But some acknowledgement of where the lines exist, between principle and guarantee, can help us to avoid those misunderstandings and potential distrust.
Being precise with language can be a huge pain sometimes. Such a huge pain that I'm confident there are at least a handful of imprecise and even incorrect statements in this post. But by striving for precision as a principle, I think we're a lot more likely to build trust with the folks we're communicating with.
If you're interested in playing with these examples, check out the Github repo.