And I'm liking it. A lot. Possibly because of the way we're using it (microservices), but probably just intrinsically, it is a language that seems to fit in the head very nicely. Not encumbered by special cases, exceptions, implicit magic and overloads. (Don't worry, I still enjoy Scala, but it's a very different Kettle[Either[Throwable, Map[String, Fish]]]).
The succinctness and elegance of Clojure is also thrown into sharp relief by the other thing I seem to be spending a lot of time on at work - grinding through a multiple-hundred-thousand-line instant-legacy untested Java codebase. This thing might have been considered state-of-the-art ten years ago when it was all about 3-tiered systems putting messages on busses - iff it had been implemented nicely, but it wasn't. As a result, it's a monolithic proliferation of POJO-manipulation, with control flow by exceptions, mutable state throughout, and impossible to test in isolation.
It can take hours to find code that actually "does something", but you have to follow the path(s) all the way down from "the top" just in case there's a bug or "hidden feature" somewhere on the way through the myriad layers with methods that look like this (anonymised somewhat):
public ListThis is everywhere. The lines that get me most annoyed are things like this:getAllFoo(Integer primaryId, Short secondaryId, String detail, Locale locale, String timeZone, String category) { if (category != null) { Map foosMap = ParameterConstants.foosMap; if (foosMap != null) { category = (foosMap.get(category.toUpperCase()) != null) ? foosMap.get(category.toUpperCase()) : category; } } List values = new ArrayList (); FooValue searchValue = new FooValue(); List fooValues = null; searchValue.setPrimaryID(primaryId); searchValue.setSecondaryId(secondaryId); searchValue.setCategory(category); try { LOGGER.info(CommonAPILoggingConstants.INF_JOBTYPE_GETALL_VALIDATION_COMPLETED); fooValues = fooDAO.getFoos(searchValue, detail); } catch (FooValidationException e) { handleException(e.getErrorId(), e); } catch (Exception e) { throw new InternalAPIException(UNKNOWN_CODE, e); } if (FULL.equalsIgnoreCase(detail)) { for (FooValue fooValue : fooValues) { Bar bar = null; try { if (StringUtils.isNotBlank(fooValue.getBarID())) { bar = barDAO.getBarByBarId(fooValue.getBarID()); fooValue.setBarName(bar.getBarName()); fooValue.setBarShortName(bar.getShortName()); LOGGER.debug(CommonAPILoggingConstants.DBG_JOBTYPE_GETALL_FETCH_BAR_BY_ID, bar.getBarName(),fooValue.getBarID()); } } catch (Exception e) { throw new InternalAPIException(UNKNOWN_CODE, e); } try { if (null != bar) { if (StringUtils.isNotBlank(bar.getBrandID())) { fooValue.setBazID(bar.getBazID()); Baz baz = bazDAO.getBazByBazId(fooValue.getBazID()); LOGGER.debug(CommonAPILoggingConstants.DBG_JOBTYPE_GETALL_FETCH_BAZ, baz.getName(),fooValue.getBazID()); fooValue.setBazName(baz.getName()); } } } catch (Exception e) { throw new InternalAPIException(UNKNOWN_CODE, e); } FooValue value = filterFooDetails(fooValue); values.add(value); } } else if (BASIC.equalsIgnoreCase(detail)) { for (FooValue fooValue : fooValues) { FooValue value = new FooValue(); value.setFooID(fooValue.getFooID()); value.setJobName(fooValue.getJobName()); value.setContentTypeName(fooValue.getContentTypeName()); value.setCategory(fooValue.getCategory()); value.setIsOneToMany(fooValue.getIsOneToMany()); values.add(value); } } else { throw new CommonAPIException(INVALID_DETAIL_PARAM,"Detail parameter value invalid"); } return values; }
fooValue.setBarName(bar.getBarName()); fooValue.setBarShortName(bar.getShortName());These x.setFoo(y.getFoo()) stanzas can go on for tens of lines. I haven't come across a name for them, so I'll call them POJO Shuffles. They suck the will-to-live out of anyone who has to navigate them as they frequently contain misalignments, micro-adjustments and hard-coding e.g.:
fooValue.setBarName(bar.getBazName()); fooValue.setBarShortName("Shortname: " + bar.getShortName()); fooValue.setBarLongName(bar.getShortName().toUpperCase());Did you notice:
- We're actually getting bazName from bar - almost certainly an autocomplete fail, but perhaps not?
- The "short name" of fooValue will actually be longer than in the source object. Is that important to something?
- There's a potential NullPointerException when we innocently try and set the "long name" of the fooValue
Then I read this gem of a paragraph from Rich Hickey, which is merely an introduction to the usage of defrecord in the official Clojure documentation, and yet reads like poetry when you've just come from code like the above: