Many moons ago, the majority of software development was done using statically typed and compiled languages, the most popular ones being C, C++, and then Java. When dynamic languages began to take off, at first they were derided as “toy” languages, an epithet most commonly associated with JavaScript. Over time, the advantages of interpreted, dynamic programming began to win the hearts of the community.
Today, especially in the web app world, you’re as likely to work with a dynamic language as with a static one. JavaScript and Python are consistently within the top five popular languages today, with others such as PHP and Ruby always in the mix.
Dynamic programming languages come with a host of benefits, including:
The astute among you will likely have noticed that pretty much every single item on this list can be interpreted as a downside as well as an upside. Taking these one at a time:
As dynamic languages have grown more popular, practitioners have had spirited discussions as to how to solve many of these problems. My own feelings about dynamic languages have slowly tempered my excitement about the freedom they provide with a strong dread of the pitfalls.
My primary work language at the moment is Ruby, which is possibly the “most dynamic” of the popular dynamic languages. However, even in Ruby, I’ve found a set of practices which I feel can help mitigate some of the downsides of dynamic programming.
Type hints exist. Use them.Over the past decade, every one of the major dynamic programming languages have introduced some kind of type hint capability. The most famous and widely-used is probably TypeScript, which is essentially a superset of JavaScript that requires a transpiler to turn the code into “regular” JavaScript. Both Python and PHP have introduced type hints as part of their standard library, and even Ruby has experimented with the RBS and Sorbet projects (sadly, there doesn’t seem to be a torrent of support for either).
Type hints are the most obvious way to make a dynamic language more static. In effect, you get the best of both worlds: you can write dynamic code but are required to be more careful about what types you expect to get and use at any point.
Good type hint systems are ironically much more complex than static languages’ type systems usually are. In Java, you can’t declare that an object’s property can be an integer or a string—but TypeScript makes this dead easy with union types:
printLabel = (label: string|number) => string { console.log(`Please fill out ${label}`);}
This is because type hint systems don’t try to turn dynamic languages into static languages; they merely provide a way for you to document your expectations in a way that machines can understand and enforce, either at compile-time or runtime.
Finally, most type hint systems allow gradual typing, where you can turn the hints on for one file at a time, rather than have to convert your entire codebase at once. You can make use of this to slowly move your code over to your type system rather than require a multi-month project to do it all together.
Dictionaries are for unknown dataA common pattern in dynamic languages is to use a dictionary or hash as a way of representing unstructured data. When returning data from a function or passing options to a function, dictionaries often are the method used:
def configure(options={}) self.logger = options[:logger] self.host_name = options[:host_name]end
or:
return { count: 5, average: 10}
These dictionaries can provide no type hints because they are meant for arbitrary key-value pairs. Using them to represent known data makes it harder to infer types or to find errors. In the first example, if you accidentally pass :loger instead of :logger, it will simply set your logger to null rather than throw an error (which is what you really want, since it’s a mistake in the calling code rather than the data).
For type hint systems that support interfaces or duck typing, such as TypeScript, you can continue to make use of this feature:
interface CountResult { count: number; average: number;}getCount : CountResult = () => { return { count: 5, average: 10} };
For languages that don’t support this, use structs or data classes. This is a simple type, often immutable, that contains a set of fields:
ConfigurationOptions = Struct.new(:logger, :host_name)def configure(options=ConfigurationOptions.new) self.logger = options.logger self.host_name = options.host_nameend
The simple move from index-notation to dot-notation introduces a level of type safety that you can get without any type hint system at all. If the method doesn’t exist, an error will be thrown and you’ll know it right away.
Even if you’re not using a type hint system, you can document your fields. Many IDEs will pick up on these comments and will help you with auto-completion. This might make for a more verbose definition, but it’s much easier to reason about:
class ConfigurationOptions # @return [Logger] attr_accessor :logger # @return [String] attr_accessor :host_name
Limit your frameworksWhat’s the difference between a framework and a library? There probably isn’t an accepted definition, but in my mind, a framework takes over your code. You’re no longer writing Ruby, you’re writing Ruby on Rails. You’re no longer writing Python, you’re writing PySpark. The code has its own look and feel that differs from your vanilla language. Libraries, by contrast, are something that you call when you need them and don’t look significantly different from your own code.
Frameworks exist in static languages, of course—Java with Spring Boot looks very different from plain Java. In dynamic languages, though, the existence of DSLs means it practically looks like you’re writing a completely different language entirely. Here’s how you define routes in Rails, for example:
Rails.application.routes.draw do resources :users, only: [:show] do post :log_in endend
My strong feeling in this area is that you should limit the number of frameworks in your app. Typically you’ll use one “big” framework (like Rails or Spark)—once you have that big framework, try not to include other dependencies that will make your code look even more different, especially if they try to be “smart” and do metaprogramming or reflection to act on your application’s code.
I’ve been guilty of this even very recently, by writing a configuration library that defines a DSL rather than a more explicit way of interacting with the configuration options. Since then I’ve come further down on the side of being explicit. Speaking of which…
Be more explicit than you need to beStatic languages enforce explicitness, often to the point of rigidity. If you want to know what a piece of code is doing, it’s easy: you ctrl-click into it and follow the call stack down. There is no “magic” involved.
For dynamic languages (and in this case I’m including languages like C that allow for defining macros), it becomes much harder to do. Dynamic languages often fail the “grep test”—if I see a method or a particular syntax, can I find where it’s defined by searching the codebase and/or dependencies? If not, it’s doing too much metaprogramming.
The only real things that should ever be doing metaprogramming or reflection are frameworks themselves. You can design your own internal framework—that’s totally fine! If it’s meant to solve a common problem unique to your team or company, that’s the sweet spot for frameworks. But don’t assume that people know every warp and weft of the effects of what you’re providing them if it’s not explicit.
How do you know if you’re not being explicit? If you look at a problem and think, “I can solve this really concisely, or I can take the time to lay out all the bits and pieces so they’re visible, and I don’t want to spend time doing that,” nearly always, you should spend time doing that.
Here’s another example. In our Rails app, there are a number of job types associated with a flyer. One way to define these associations is by looping over the job types:
JOB_TYPES = [:process_image, :upload_image, :process_tagging]JOB_TYPES.each do |type| has_many "#{type}_jobs"end
This works! But if I see flyer.process_image_jobs somewhere in my codebase, how on earth am I going to know where it comes from?
It’s more work—but more informative—to be explicit:
has_many :process_image_jobshas_many :upload_image_jobshas_many :process_tagging_jobs
In general, try not to violate the Principle of least astonishment. This means:
this keyword in JavaScript outside the scope of a class). Be explicit about what object is being worked on.There is definitely a time and a place for being fancy. Choose your spots wisely!
Don’t be temptedDynamic languages give you plenty of freedom—enough rope to hang yourself, as they say. Enjoy the freedom, but try to think more statically when you can.
The post Adding structure to dynamic languages appeared first on Stack Overflow Blog.