The need for shorter and shorter Time-To-Market requires to integrate more and more third-party libraries. There's no time for the NIH syndrom anymore if it ever was. While most of the time, the library's API is ready to use, it happens that one needs to "adapt" it to the codebase sometimes. How easy the adaptation is depends a lot on the language.
For example, in the JVM, there are a couple of Reactive-Programming libraries: RxJava, Project Reactor, Mutiny, and coroutines. You might need a library that uses types of one library, but you based your project on another.
In this post, I'd like to describe how to add new behavior to an existing object/type. I won't use any reactive type to make it more general but add toTitleCase()
to String
. When it exists, inheritance is not a solution as it creates a new type.
I apologize in advance that the below implementations are pretty simple: they are meant to highlight my point, not to handle corner cases, e.g., empty strings, non-UTF 8, etc.
JavaScript
JavaScript is an interpreted dynamically- and weakly-typed language, which runs the World Wide Web - until WASM takes over? As far as I know, its design is unique, as it's prototype-based. A prototype is a mold for new "instances" of that type.
You can easily add properties, either state or behavior, to a prototype.
Object.defineProperty(String.prototype, "toTitleCase", {
value: function toTitleCase() {
return this.replace(/\w\S*/g, function(word) {
return word.charAt(0).toUpperCase() + word.substr(1).toLowerCase();
});
}
});
console.debug("OncE upOn a tImE in thE WEst".toTitleCase());
Note that objects created from this prototype after the call to defineProperty
will offer the new property; objects created before won't.
Ruby
Ruby is an interpreted dynamically- and strongly-typed language. While not as popular as it once was with the Ruby On Rails framework, I still use it with the Jekyll system that powers this blog.
Adding methods or attributes to an existing class is pretty standard in the Ruby ecosystem. I found two mechanisms to add a method to an existing type in Ruby:
-
Use class_eval:
Evaluates the string or block in the context of mod, except that when a block is given, constant/class variable lookup is not affected. This can be used to add methods to a class
Just implement the method on the existing class.
Here's the code for the second approach:
class String
def to_camel_case()
return self.gsub(/\w\S*/) {|word| word.capitalize()}
end
end
puts "OncE upOn a tImE in thE WEst".to_camel_case()
Python
Python is an interpreted dynamically- and strongly-typed language. I guess every developer has heard of Python nowadays.
Python allows you to add functions to existing types - with limitations. Let's try with the str
built-in type:
import re
def to_title_case(string):
return re.sub(
r'\w\S*',
lambda word: word.group(0).capitalize(),
string)
setattr(str, 'to_title_case', to_title_case)
print("OncE upOn a tImE in thE WEst".to_title_case())
Unfortunately, the above code fails during execution:
Traceback (most recent call last):
File "<string>", line 9, in <module>
TypeError: can't set attributes of built-in/extension type 'str'
Because str
is a built-in type, we cannot dynamically add behavior. We can update the code to cope with this limitation:
import re
def to_title_case(string):
return re.sub(
r'\w\S*',
lambda word: word.group(0).capitalize(),
string)
class String(str):
pass
setattr(String, 'to_title_case', to_title_case)
print(String("OncE upOn a tImE in thE WEst").to_title_case())
It now becomes possible to extend String
, because it's a class we have created. Of course, it defeats the initial purpose: we had to extend str
in the first place. Hence, it works with third-party libraries.
With interpreted languages, it's reasonably easy to add behavior to types. Yet, Python already touches the limits because the built-in types are implemented in C.
Java
Java is a compiled statically- and strongly-typed language that runs on the JVM. Its static nature makes it impossible to add behavior to a type.
The workaround is to use static
methods. If you've been a Java developer for a long time, I believe you probably have seen custom StringUtils
and DateUtils
classes early in your career. These classes look something like that:
public class StringUtils {
public static String toCamelCase(String string) {
// The implementation is not relevant
}
// Other string transformations here
}
I hope that by now, using Apache Commons and Guava have replaced all those classes:
System.out.println(WordUtils.capitalize("OncE upOn a tImE in thE WEst"));
In both cases, the usage of static methods prevents fluent API usage and thus impairs developer experience. But other JVM languages do offer exciting alternatives.
Scala
Like Java, Scala is a compiled, statically- and strongly-typed language that runs on the JVM. It was initially designed to bridge between Object-Oriented Programming and Functional Programming. Scala provides many powerful features. Among them, implicit classes allow adding behavior and state to an existing class. Here is how to add the toCamelCase()
function to String
:
import Utils.StringExtensions
object Utils {
implicit class StringExtensions(thiz: String) {
def toCamelCase() = "\\w\\S*".r.replaceAllIn(
thiz,
{ it => it.group(0).toLowerCase().capitalize }
)
}
}
println("OncE upOn a tImE in thE WEst".toCamelCase())
Though I dabbled a bit in Scala, I was never a fan. As a developer, I've always stated that a big part of my job was to make implicit requirements explicit. Thus, I frowned upon the on-purpose usage of the implicit
keyword. Interestingly enough, it seems that I was not alone. Scala 3 keeps the same capability using a more appropriate syntax:
extension(thiz: String)
def toCamelCase() = "\\w\\S*".r.replaceAllIn(
thiz,
{ it => it.group(0).toLowerCase().capitalize }
)
Note that the bytecode is somewhat similar to Java's static method approach in both cases. Yet, API usage is fluent, as you can chain method calls one after another.
Kotlin
Like Java and Scala, Kotlin is a compiled, statically- and strongly-typed language that runs on the JVM. Several other languages, including Scala, inspired its design.
My opinion is that Scala is more powerful than Kotlin, but the trade-off is an additional cognitive load. On the opposite, Kotlin has a lightweight approach, more pragmatic. Here's the Kotlin version:
fun String.toCamelCase() = "\\w\\S*"
.toRegex()
.replace(this) {
it.groups[0]
?.value
?.lowercase()
?.replaceFirstChar { char -> char.titlecase(Locale.getDefault()) }
?: this
}
println("OncE upOn a tImE in thE WEst".toCamelCase())
If you wonder why the Kotlin code is more verbose than the Scala one despite my earlier claim, here are two reasons:
- I don't know Scala well enough, so I didn't manage corner cases (empty capture, etc.), but Kotlin leaves you no choice
- The Kotlin team removed the
capitalize()
function from thestdlib
in Kotlin 1.5
Rust
Last but not least in our list, Rust is a compiled language, statically and strongly typed. It was initially designed to produce native binaries. Yet, with the relevant configuration, it also allows to generate Wasm. In case you're interested, I've taken link:/focus/start-rust/[a couple of notes] while learning the language.
Interestingly enough, though statically-typed, Rust also allows extending third-party APIs as the following code shows:
trait StringExt { // 1
fn to_camel_case(&self) -> String;
}
impl StringExt for str { // 2
fn to_camel_case(&self) -> String {
let re = Regex::new("\\w\\S*").unwrap();
re.captures_iter(self)
.map(|capture| {
let word = capture.get(0).unwrap().as_str();
let first = &word[0..1].to_uppercase();
let rest = &word[1..].to_lowercase();
first.to_owned() + rest
})
.collect::<Vec<String>>()
.join(" ")
}
}
println!("{}", "OncE upOn a tImE in thE WEst".to_camel_case());
- Create the abstraction to hold the function reference. It's known as a trait in Rust.
- Implement the trait for an existing structure.
Trait implementation has one limitation: our code must declare at least one of either the trait or the structure. You cannot implement an existing trait for an existing structure.
Conclusion
Before writing this post, I thought that interpreted languages would allow extending external APIs, while compiled languages wouldn't - with Kotlin the exception. After gathering the material, my understanding has changed drastically.
I realized that all mainstream languages provide such a feature. While I didn't include a C# section, it also does. My conclusion is sad, as Java is the only language that doesn't offer anything in this regard.
I've regularly stated that Kotlin's most significant benefit over Java is extension properties/methods. While the Java team continues to add features to the language, it still doesn't offer a developer experience close to any of the above languages. As I've used Java for two decades, I find this conclusion a bit sad, but it's how it is, unfortunately.
To go further:
Originally published at A Java Geek on November 7th, 2021
Top comments (0)