[HN Gopher] Six Years of Professional Clojure
___________________________________________________________________
Six Years of Professional Clojure
Author : erez-rabih
Score : 183 points
Date : 2021-08-02 11:52 UTC (11 hours ago)
(HTM) web link (engineering.nanit.com)
(TXT) w3m dump (engineering.nanit.com)
| EdwardDiego wrote:
| > An incoming HTTP request? it is a plain Clojure dictionary.
|
| I learned to code in Python. Loved it. Dynamically typed dicts up
| the wazoo!
|
| Then I learned why I prefer actual types. Because then when I
| read code, I don't have to read the code that populates the dicts
| to understand what fields exist.
| [deleted]
| robertlagrant wrote:
| I agree. This doesn't seem much different to saying they're all
| objects. You still need to know what to expect inside the
| dictionary.
| goatlover wrote:
| The difference being that objects have a class where you can
| look to see what fields it specifies.
| robertlagrant wrote:
| Sure, depending on the language. What I mean is having
| dictionaries doesn't mean you don't have to learn schemas.
| dan-robertson wrote:
| Java doesn't really have a nice interface for interacting
| with objects in general. Closure _does_ have a nice
| interface for interacting with dictionaries. They have
| namespaces keyword symbols for keys which are much more
| ergonomic than typing strings, and they have lots of
| functions for modifying dictionaries. I think the big
| difference is in the philosophy of what the language thinks
| data is, and how the world ought to be modelled.
| lvh wrote:
| The two are not mutually exclusive. Clojure has namespaced
| keywords and specs[0] to cover that. (There is also the third-
| party malli, which takes a slightly different appproach.)
|
| The advantage is that maps are extensible. So, you can have
| middleware that e.g. checks authentication and authorization,
| adds keys to the map, that later code can check it directly.
| Namespacing guarantees nobody stomps on anyone else's feet.
| Spec/malli and friends tell you what to expect at those keys.
| You can sort of do the same thing in some other programming
| languages, but generally you're missing one of 1) typechecking
| 2) namespacing 3) convenience.
|
| [0]: spec-ulation keynote from a few years ago does a good job
| explaining the tradeoffs;
| https://www.youtube.com/watch?v=oyLBGkS5ICk
| kitd wrote:
| Yeah, he mentions that later on as a drawback
| dan-robertson wrote:
| Question: 1. Can a GET request have a non-empty request body?
|
| 2. Assuming you don't know the answer to that question, will
| the type system you use be able to tell you the answer to that
| question?
|
| This is a pretty simple constraint one might want (a constraint
| that only certain requests have a body) but already a lot of
| static type systems (e.g. the C type system) cannot express and
| check it. If you can express that constraint, is it still easy
| to have a single function to inspect headers on any request?
| What about changing that constraint in the type system when you
| reread the spec? Is it easy?
|
| The point isn't that type systems are pointless but that they
| are different and one should focus on what the type system can
| do for you, and at what cost.
| lkitching wrote:
| Any statically-typed language with generics can express that
| by parameterising the request type with the body type. A
| bodiless request is then just Request[Nothing] (or
| Request[Unit] if your type system doesn't have a bottom
| type). Accessing the headers just requires an interface which
| all static languages should be able to express.
| dan-robertson wrote:
| (1) note that "statically-typed language with generics"
| excludes a lot of statically typed languages, including C
| and Go (at least pre generics).
|
| (2) this misses the meat of the question which is how to
| express that (eg) a _GET_ request doesn't come with a body
| and a _POST_ request does. I suppose that you're suggesting
| that one registers a url handler with a method type and
| that forces the handler to accept responses of a certain
| type. Or perhaps you are implicitly allowing for sun types
| (which aren't a thing in many static type systems.)
|
| (3) even in C++, isn't this suggestion hard to work with.
| That is, isn't it annoying to write a program which works
| for any request whether or not it has a body because the
| type of the body must be a template parameter that adds
| templates to the type of every method which is generic to
| it. But maybe that is ok or I just don't understand C++.
| xapata wrote:
| How about values restricted to identifiers currently in the
| database table? There's always something the type system
| can't do.
| [deleted]
| jolux wrote:
| F# has a feature called type providers that make this
| sort of bookkeeping between the database and the code
| less tedious, but even if you mess it up, static typing
| still gives you more safety than dynamic. If your code
| blew up because it should have accepted an identifier it
| didn't, you know that the code has not been written to
| handle that case and can fix it. Alternatively, you can
| just choose to ignore this, and do what a dynamic
| language does. There is nothing stopping you from being
| dynamic in a static language, passing everything around
| as a map, etc.
| dharmaturtle wrote:
| A demo of a SQL type provider in action:
| https://youtu.be/RK3IGYNZDPA?t=2539
|
| It requires a bit of elbow grease to make it work with a
| CICD system... but it works :D
| twic wrote:
| 1. Yes. It's weird, but it's legal HTTP.
|
| 2. Sure. The request type has a body property.
| dan-robertson wrote:
| Does "the request type has a body property" actually imply
| (1) though? In a language like C or C++ or Java, you could
| have a protocol like "body is always null on GET requests."
| The question isn't really about HTTP, that was just an
| easy-to-reach-for example, it is really about what having
| explicit types allows one to deduce about a program.
| taeric wrote:
| To be fair, an incoming request is, almost by definition,
| dynamic. It makes sense to have that as a map, since the main
| sensible thing to do on receipt is validation/inspection.
|
| Granted, you may have a framework do a fair bit of that.
| Depends how much you want between receipt of the request and
| code you directly control.
| Zababa wrote:
| Usually the approach in a statically-typed language is to
| transform your dynamic request into something that you know
| through parsing instead of validation. Here's a great article
| about this: https://lexi-
| lambda.github.io/blog/2019/11/05/parse-don-t-va....
| taeric wrote:
| That is a valid approach in any language. Static or not.
| Doesn't change my point that heavily. And it is all too
| possible to pick a bad parsing/binding language such that
| protocol changes in the request are now foot guns.
| Zababa wrote:
| That's true, but static languages are not worse at
| handling dynamic data. From the same author:
| https://lexi-lambda.github.io/blog/2020/01/19/no-dynamic-
| typ....
| taeric wrote:
| To an extent, I agree. I'm just pointing out that this is
| a bit of a bad example. I want there to be dynamic
| inspection of input.
|
| That said, maps as the only tool is clearly messy. And is
| a straw man.
| kingdomcome50 wrote:
| This is the second time I've seen the link above. And while
| I agree with the premise, the author _clearly_ does not
| understand how to properly use the `Maybe` monad (a term
| that does not make an appearance!).
|
| There is little use in wrapping a call in `Maybe` to then
| _immediately_ unwrap the result on the next line. Doing so
| isn 't really using the construct... One would expect the
| lines following the creation of `Maybe` to _bind_ calls
| through the monad.
|
| In the end I see almost no meaningful difference between
| their "Paying it forward" example and simply utilizing an
| `if` to check the result and throw. In essence the author
| is using a parse _and_ validate approach!
| travv0 wrote:
| Lexi _absolutely_ understands how to properly use the
| Maybe monad. What you 're saying to do here is the exact
| opposite of what this post is advocating for. You're
| talking about pushing the handling of the Maybe till
| later and the post is all about the advantages of
| handling it upfront and not having to worry about it
| anymore. You might want to read it one more time.
| kingdomcome50 wrote:
| I understand. But what is purpose of `Maybe`? The reason
| one would reach to the above construct is _precisely_ to
| offload (pushing to later) the handling of a value that
| may (or may not) be present at runtime such that a
| developer can write code assuming the value is always
| present and ignore the `Nothing` case.
|
| Sure you can unwrap it right away, but that isn't
| necessary because you could _also_ just "bind" the next
| function call to the monad (which is more idiomatic to
| the construct). You _never_ have to worry about that
| value in this case because... well... that 's the benefit
| of using `Maybe`.
|
| I'm not super familiar with Haskell, but my sense is that
| the author is trying more to please the compiler ( _at a
| specific point in the program!_ ) than simplify the
| logic. That is, they want a concrete value (`configDirs`)
| to exist in the body of `main` more than they want the
| cleanest representation of the problem in code.
| travv0 wrote:
| > But what is purpose of `Maybe`?
|
| In this case, it's to provide a better error message in
| case there's an empty list than `fromList` would provide.
|
| > You never have to worry about that value in this case
| because... well... that's the benefit of using `Maybe`.
|
| But you do, your entire program doesn't live in `Maybe`
| so at some point you have to check whether it's `Just a`
| or `Nothing`. Once again, the whole point of the post is
| to argue that getting out of the `Maybe` as close to
| parsing time as possible is preferable so you have a more
| specific type to work with after that. You also see right
| away what didn't parse instead of just knowing that
| _something_ didn 't parse, which is what would happen if
| you stayed in the `Maybe` monad for all your parsing.
| kingdomcome50 wrote:
| > your entire program doesn't live in `Maybe`
|
| Well... if your entire program is dependent on some input
| that may or may not exist at runtime... then it kind of
| _does_ live in `Maybe`.
|
| I have no issue with unwrapping a `Maybe` to throw an
| exception. But I _do_ find it a bit ironic that the post
| is about parsing instead of validating, that the perfect
| construct is _right there_ to exemplify how it could be
| done, but the author then chooses to eschew it and
| instead show examples of how validation could look.
|
| The body of `main`, for example, could be refactored to
| something like: maybeInitialized <-
| (getConfigurationDirectories >>= head >> initializeCache)
|
| Which actually _shows_ how `Maybe` can be used to
| simplify the system. If you want to unwrap the maybe at
| this point to throw, go for it! But the above is a _much_
| cleaner representation of the program than what author is
| trying to do (it 's crystal clear how the cache _might_
| get initialized). I would expect "Parse don't validate"
| to be about how useful `Maybe` is to combine parsing
| logic into a functional flow vs. how validation leads to
| an ugly procedural approach.
| garethrowlands wrote:
| I think you're referring to this part of the
| `getConfigurationDirectories` action, which has type `IO
| (NonEmpty FilePath)`: case nonEmpty
| configDirsList of Just nonEmptyConfigDirsList
| -> pure nonEmptyConfigDirsList Nothing ->
| throwIO $ userError "CONFIG_DIRS cannot be empty"
|
| The "meaningful difference" you're looking for is the
| type of `getConfigurationDirectories`. The previous
| version had type `IO [FilePath] `, which _doesn't_
| guarantee any configuration directories at all. It did
| indeed check the results and throw. But it doesn't
| guarantee that all the `[FilePath]` values in the program
| have been checked. There are neither tests nor proofs in
| this code. In contrast, with the revised version, you can
| be certain anywhere you see a `NonEmpty FilePath` it is
| indeed non-empty.
|
| The code I've quoted that checks which case we have, is
| the only place that needs to handle that `Maybe`. Or
| maybe `main`, if we want to be more graceful. The author
| (I wouldn't say I know her but I know that much) does
| know how to chain maybes with bind but it's not necessary
| in this example code.
| kingdomcome50 wrote:
| My point is that if you are not chaining `Maybe` then the
| utility of employing the construct is unobserved. The
| entire _purpose_ of using `Maybe` is to relieve the
| client from the need to make checks at every call for a
| value that may (or may not) exist. If you intend to
| immediately "break out" of the monad and (even more
| specifically) throw an error, you might as well just use
| an `if`.
|
| I'm sure `main` _could_ be written to "bind"/"map"
| `getConfigurationDirectories` with `nonEmpty`, `head`,
| and `initializeCache` in a way that puts the `throw` at
| the top-level (of course the above implementations may
| need to change as well). Unfortunately I'm not familiar
| enough with Haskell to illustrate it myself.
| lkitching wrote:
| The purpose of Maybe is to explicitly represent the
| possible non-existence of a value which in Haskell is the
| only option since there's no null value which inhabits
| every type. The existence of the monad instance is
| convenient but it's not fundamental. The type of
| getConfigurationDirectories could be changed to MaybeT IO
| (NonEmpty FilePath) to avoid the match but I don't think
| it would make such a small example clearer.
| kingdomcome50 wrote:
| There are numerous ways to redesign the function
| signatures, but I would imagine the simplest would be
| (again, idk Haskell syntax):
| getConfigurationDirectories: unit -> Maybe [FilePath]
| nonEmpty: [a] -> Maybe [a] head: [a] -> Maybe a
| initializeCache: FilePath -> unit
|
| Notice `nonEmpty` isn't really necessary because `head`
| could to the work. The above could be chained into a
| single, cohesive stack of calls where the result of each
| is piped through the appropriate `Maybe` method into the
| next call in a point-free style. I cannot imagine how
| this wouldn't be clearer. e.g:
| maybeInitialized <- (getCofigurationDirectories >>= head
| >> initializeCache)
|
| That's the whole thing. Crystal clear. The big takeaway
| of "Parse don't validate" should be about the predominant
| use of the `Maybe` monad as a construct to make "parsing"
| as ergonomic as possible! Each function that returns
| `Maybe` can be understood as a "parser" that, of course,
| can be elegantly combined to achieve your result.
|
| My critique is exactly that unwrapping the `Maybe`
| immediately in order to throw an exception is kind of the
| worst of both worlds. I mentioned this in a sibling
| comment, but my sense is that the author is more
| concerned with have a concrete value (`configDirs`)
| available in the scope of `main` than best-representing
| the solution to the problem in code. It is a shame
| because I _agree_ with the thesis.
| lkitching wrote:
| On the contrary the The NonEmpty type is fundamental to
| the approach in that example since it contains in the
| type the property being checked dynamically (that the
| list is non-empty). The nonEmpty function is a simple
| example of the 'parse don't validate' approach since it
| goes from a broader to a more restricted type, along with
| the possibility of failure if the constraint was not
| satisfied. The restriction on the NonEmpty type is what
| allows NonEmpty.head to return an a instead of a (Maybe
| a) and thus avoid the redundant check in the second
| example. The nonEmpty in your alternative implementation
| is only validating not parsing since after checking the
| input list is non-empty, it immediately discards the
| information in the return type. This forces the user to
| deal with a Nothing result from head that can never
| happen. Attempting to clean the code up by propagating
| Nothing values using bind is just hiding the problem that
| the validating approach avoids entirely.
| dharmaturtle wrote:
| You might try re-reading it with some charity - the
| example's purpose isn't to teach the `Maybe` monad, but
| to remove the redundant check. To go into what `bind`
| does would be a diversion from the main topic (parsing vs
| validating).
|
| FWIW SPJ has called this blog's author a "genius" so... I
| think they do know how `Maybe` works. https://gitlab.hask
| ell.org/ghc/ghc/-/issues/18044#note_26617...
| kingdomcome50 wrote:
| But `Maybe` is specifically designed to remove redundant
| checks for a value that may (or may not) be present!
| That's the whole point of the monad! It seems rather
| unfortunate this isn't highlighted (or at least
| illustrated) doesn't it?
|
| I generally _agree_ with the premise of the post.
| fmakunbound wrote:
| This is one of those self-inflicted Clojure problems. In Common
| Lisp you might use an alist or a plist for small things, but
| you'd definitely reach for CLOS classes for things that had
| relationships to other things and things that had greater
| complexity.
|
| IIRC, the preference for complecting things via maps, and then
| beating back the hordes of problems with that via
| clojure.spec.alpha (alpha2?) is a Hickey preference. I don't
| recall exactly why.
| blacktriangle wrote:
| No source to back this up, but my guess is that Clojure was
| driven by the need to interopt with Java so is to not get
| kicked out of production. This meant absorbing the Java
| object model. Shipping a language with both Java objects and
| CLOS and making them both play nice together sounds like a
| nightmare.
| joncampbelldev wrote:
| This comment helpfully explains many of the reasons Rich had
| for choosing immutable, persistent, generic data structures
| as the core information model in clojure (instead of concrete
| objects / classes):
| https://news.ycombinator.com/item?id=28041219
|
| Not wanting to misquote the above / Rich himself I would TLDR
| it to:
|
| - flexibility of data manipulation
|
| - resilience in the face of a changing outside world
|
| - ease of handling partial data or a changing subset of data
| as it flows through your program
|
| Please note that no one (I hope) is saying that the above
| things are impossible or even necessarily difficult with
| static typing / OOP. However myself and other clojurists at
| least find the tradeoff of dynamic typing + generic maps in
| clojure to be a net positive especially when doing
| information heavy programming (e.g. most business
| applications)
| tragomaskhalos wrote:
| Namedtuples FTW! A de-facto immutable dict with the keys listed
| right there in the definition to obviate all the usage head-
| scratching. Then, if you need more functionality (eg factory
| functions to fill in sensible defaults), you can just subclass
| it.
|
| TBH I've never understood the attraction of the untyped dict
| beyond simple one-off hackups (and even there namedtuples are
| preferable), because like you say you typically have no idea
| what's supposed to be in there.
| lmilcin wrote:
| > Pure functions make code design easier: In fact, there's very
| little design to be done when your codebase consists mostly of
| pure functions.
|
| Ummm... I am a little bit fearful about your codebase.
|
| If you don't see the need for designing your FP system it
| probably mostly means it is being designed ad hoc rather than
| explicitly.
|
| If you are trying to compare to OOP system done right, you will
| notice that this includes a lot of work in identifying domain
| model of your problem, discovering names for various things your
| application operates on, and so on. Just because you elect to not
| do all of this doesn't mean the problem vanishes, it most likely
| is just shifted to some form of technical debt.
|
| > Clojure is a dynamic language which has its advantages but not
| once I stumbled upon a function that received a dictionary
| argument and I found myself spending a lot of time to find out
| what keys it holds.
|
| Dynamic typing is a tradeoff which you have to be very keenly
| aware of if you want to design a non-trivial system in a
| dynamically typed language.
|
| It is not a problem with Clojure, it is just a property of all
| dynamically-typed languages.
| dmitriid wrote:
| One thing I don't like about all articles on clojure is that
| basically all of them say: ah, it's just like lisp with lists
| `(an (example of) (a list))` with vectors `[1 2 3]` thrown in. So
| easy!
|
| But then you get to Clojure proper, and you run into additional
| syntax that either convention or functions/macros that look like
| additional syntax.
|
| Ok, granted, -> and ->> are easy to reason about (though they
| look like additional syntax).
|
| But then there's entirely ungooglable ^ that I see in code from
| time to time. Or the convention (?) that call methods on Java
| code (?) with a `.-`
|
| Or atoms defined with @ and dereferenced with *
|
| Or the { :key value } structure
|
| There's way more syntax (or things that can be perceived as
| syntax, especially to beginners) in Clojure than the articles
| pretend there is. (defn ^:export db_with [db
| entities] (d/db-with db (entities->clj entities)))
| (defn entity-db "Returns a db that entity was created
| from." [^Entity entity] {:pre [(de/entity?
| entity)]} (.-db entity)) (defn ^:after-
| load ^:export refresh [] (let [mount
| (js/document.querySelector ".mount") comp (if
| (editor.debug/debug?) (editor.debug/ui
| editor) (do
| (when (nil? @*post) (reset! *post (->
| (.getAttribute mount "data") (edn/read-string))))
| (editor *post)))] (rum/mount comp mount)))
| ronnier wrote:
| Single engineers will pick clojure at companies , build a
| project in it, later that engineer will move on, now nobody can
| maintain this code so it's rewritten in some normal language.
| I've seen that happen a few times. That code is hard to read
| and understand. This is why clojure will remain niche.
| achikin wrote:
| It could have been Go and Java programmer trying to
| understand it. Or it could have been some clumsy tool written
| in node which Go programmer finds hard to read and
| understand. Clojure's main advantage is that you can you can
| learn it very very quickly up to the point when you
| understand most of the code, the language is very very small
| compared to "five main languages".
| outworlder wrote:
| > Single engineers will pick clojure at companies , build a
| project in it, later that engineer will move on, now nobody
| can maintain this code so it's rewritten in some normal
| language
|
| "Normal language"?
|
| You mean, whatever language is most popular at the company.
| What's "normal" at one would be completely alien at another.
| Even things like Java. If you don't have anything in the Java
| ecosystem, the oddball Java app will be alien and will likely
| get rewritten into something else.
|
| The reason Clojure remains niche is that some people somehow
| think it's not a "normal" language, for whatever reason.
| taeric wrote:
| That is possible with all languages. I've seen java, scala,
| clojure, perl, python, etc.
|
| Usually this is made worse by bespoke build tools and
| optimizations that make the system punishing to pick up.
| sramsay wrote:
| You've seen a case where someone wrote something in Python
| that later devs could not understand and then rewrote it in
| . . . what? And you've seen that with Java?
|
| There's a big difference between a developer going off and
| writing something in one of the top five most used
| languages in the world and doing so in Scala.
| taeric wrote:
| Yes. I've seen and contributed to dumpster fires in all
| of those languages. I would love to say it was all some
| rogue developer that crapped on things, but it is often
| just new developers. The more, the more damage.
| lostcolony wrote:
| Both are strange and alien to Javascript developers, who
| can be full stack.
|
| Python may seem simple once you know it, but going in
| blind there's plenty of traps to bite you. Significant
| whitespace for one.
| chrsig wrote:
| I think there's two different issues:
|
| 1. picking a language/tool that a company doesn't have
| personnel with experience using it
|
| 2. picking a language/tool that is esoteric, which
| generally implies #1 as well.
|
| #1 on its own isn't great, but generally when sticking in
| the java/python/ruby/javascript/php/etc...mainstream
| languages, there's a lot more documentation, and there's
| a higher chance that _someone_ in the company will have
| some familiarity. If nothing else, it'd be easier to hire
| a replacement for.
| lostcolony wrote:
| A higher chance, yes, but it doesn't matter much; what is
| tricky with most applications is the domain. Certainly,
| it's faster to go learn a language than to learn a new
| domain. To that end, you can get the whole team trained
| faster in a language than you can hire someone with
| experience and train them to the domain.
| joelbluminator wrote:
| > Certainly, it's faster to go learn a language than to
| learn a new domain.
|
| It's not only the language but the framework. For example
| I know javascript well enough but I now am quite a noob
| with Ember in my new role. I would say the framework is
| just as important as the language, at least when doing
| web development.
| chrsig wrote:
| You're kind of reinforcing the point though -- now you've
| got a whole team distracted by picking up a new
| language....why? how is it a good use of anyone's time?
| And it'll be a perennial training issue in the case of an
| esoteric language, because those team members will
| eventually turn over as well, meaning that you don't get
| to avoid either hiring or training a new person on it.
|
| If it's just one component, implemented by a single dev,
| it really can make more sense to understand what it does
| and rewrite it in a language that's common in the
| company.
| lostcolony wrote:
| I'm not advocating NOT rewriting it. I'm just saying,
| back to the great grandparent's point, that the issue is
| a dev went rogue, NOT the language the rogue dev chose.
| The difficulty is the same regardless of the language the
| rogue dev chose; it's not that they picked Clojure, it's
| that they picked a language there was no organizational
| adoption of.
| agumonkey wrote:
| is it really hard to read (could be) or is it just that the
| average coder never saw lisp or sml and doesn't want to
| bother bearing the responsibility to learn something alien on
| duty ?
| mollusk wrote:
| You need a team that wants to use Clojure. I wrote Clojure
| professionally for 2 years, and everyone at the company was
| excited about it and sold on the language. Even after 3-5
| years of programming in it. Now, at a different place, we
| write in a different language, and even though I still love
| Clojure, I'm not gonna write some project in it, even if
| Clojure might suit it so well, because I know these people
| are sold on different language, and I'm not going to preach
| and I'm not going to make their lives more difficult by
| having to maintain some obscure codebase.
| lvh wrote:
| Minor point of order about the atoms: they're not defined with
| @ nor derefd with _. If you 're referring to _earmuffs* that's
| convention not syntax (specifically for dynamically scoped
| variables, which could be atoms or anything else), and @ is
| indeed deref. (More specifically @x is a reader macro ish that
| expands to literally `(deref x)`.)
| dmitriid wrote:
| Thank you! I never seem to remember this (but I don't use
| Clojure, so it's not an ingrained knowledge)
| girishso wrote:
| Agreed. These days I'm really fascinated by clojure and trying
| to learn clojure. Other than the project setup and repl and the
| editor (which I had considered), these weird characters are
| throwing me off.
|
| What clojure really needs is some kind of opinionated framework
| or starter template, something like create-react-app. That has
| all these things figured out so a beginner like me can start
| playing with actual clojure, which documents all the steps to
| setup the repl and editor and what not. The last time I asked
| for this I was told about lein templates, they help but there's
| no documentation to go with those.
|
| There needs to be some push from the top level. create-react-
| app was produced by facebook. Elm reactor (which lets you just
| create a .elm file and play with elm) was created by Evan the
| language creator himself.
|
| tldr: There's a huge barrier to start playing with clojure that
| needs to come down and the push needs happen from the top
| level.
| uDontKnowMe wrote:
| There is the widely used Luminus framework
| https://luminusweb.com/
| girishso wrote:
| Yes, of course and I've got the book as well. The problem
| with the book is I got stuck on the _very first_ code
| example in the book. I know there 's a forum for the book
| where ( _hopefully_ ) I can get my query answered.
|
| My point is: these are all individual attempts (the book i
| mean) and there will always be something on page xyz broken
| and it can't be solved by individuals. To solve these
| problems, there needs to be constant time and money
| investment from someone serious (like facebook in case of
| create-elm-app).
| uDontKnowMe wrote:
| Yes I agree there is a problem of a lack of institutional
| funding in the Clojure world. Luminus is a great tool but
| it is a bit sad that it is arguably the most production-
| ready web toolkit in the ecosystem and it is mostly the
| work of a single person.
|
| There is some community effort to better fund the core
| infrastructure in Clojure through
| https://www.clojuriststogether.org/, hopefully they can
| continue to attract more funding developers and
| companies.
|
| In general a lot of these issues could be alleviated if
| the community was just in general larger with more
| contributors. I think the Clojure community is quite
| welcoming to newbies in the sense that people are quite
| responsive, kind and helpful around the internet, in
| Clojurians Slack (try asking there btw, if you haven't
| yet and are still stuck at the start of the book), etc.
| But in other ways people seem averse to criticism or
| suggestions from outsiders. I think the Clojure world
| needs to do a bit of self reflection to understand why
| adoption is so low right now and honestly consider what
| needs to change to attract more developers and
| contributors.
| cr__ wrote:
| Not sure how you're supposed to find this page, but it's pretty
| useful: https://clojure.org/guides/weird_characters
| dmitriid wrote:
| Nice! I missed it (or it didn't exist) when I last looked at
| Clojure a few years back
| roenxi wrote:
| You missed it, it has been there forever. But it says good
| and bad things about Clojure that its reference
| documentation is one of its weakest points.
|
| The Guide/Reference split obscures a lot of information (do
| I want guidance on Deps & CLI or do I want reference on
| Deps & CLI?) and the guides where that gem is hidden
| randomly mix advanced topics (eg, how to set up generative
| testing), beginner topics (how to read Clojure code) and
| library author topics (eg, Reader Conditionals).
|
| When you think about it, there is nearly no trigger to look
| at the guides when the information you need is there.
| Clojure is a weird mix of both well documented and terribly
| documented. All the facts are on the website, very few of
| them are accessible when required. The people who make it
| past that gauntlet are rewarded by getting to use Clojure.
| dmitriid wrote:
| In my case it was even worse, as I started with
| ClojureScript, and official documentation was simply
| abysmal then.
| hcarvalhoalves wrote:
| {:pre [(de/entity? entity)]}
|
| is "syntactic sugar" for (hash-map (keyword
| "pre") (vector (de/entity? entity)))
|
| while (.getAttribute mount "data")
|
| is calling the method `.getAttribute` on the `mount` object -
| since it's a Lisp, it's in prefix notation. It also highlights
| how methods are not special and just functions that receive the
| object as first argument.
|
| Finally, @*post
|
| is the same as (deref *post)
|
| and the `*` means nothing to the language - any character is
| valid on symbol names, the author just chose an asterisk.
|
| Most of what you believe to be syntax are convenience "reader
| macros" (https://clojure.org/reference/reader), and you can
| extend with your own. You can write the same code without any
| of it, but then you'll have more "redundant" parenthesis.
| dmitriid wrote:
| > Most of what you believe to be syntax are convenience
| "reader macros"
|
| And yet, you need to know what all those ASCII symbols mean,
| where they are used, and they are indistinguishable from
| syntax.
|
| Moreover, even Clojure documentation calls them syntax. A
| sibling comment provided a wonderful link:
| https://clojure.org/guides/weird_characters
| MeteorMarc wrote:
| What build tools do you use, maven?
| finalfantasia wrote:
| Clojure developers tend to choose the official Clojure CLI
| tools [1] for new projects these days.
|
| [1] https://clojure.org/guides/deps_and_cli
| tribaal wrote:
| Not the author, but most clojure projects use leiningen to
| build and distribute projects (https://leiningen.org/)
|
| This seems to be the case for the author's open-source work
| (https://github.com/nanit/kubernetes-custom-
| hpa/blob/master/a...)
| evanspa wrote:
| Great article, love Clojure. Was trying to figure out what Nanit
| does. Might want to consider putting a link to the Nanit homepage
| on your engineering page. When just typed in nanit.com and saw
| the baby monitor tech, I thought maybe I went to the wrong place,
| until I saw the logos matched. Anyway, good read, but please put
| a link to your home page on your engineering site, or, put a 1
| liner in the opening of your blog giving context to what your
| company does.
| altrunox wrote:
| Great article, love Clojure, unfortunately couldn't find any work
| with it when I tried, I managed to flop in the only interview I
| got :( Still, I miss it sometimes when I'm writing C#.
| cgopalan wrote:
| Another good report about what Clojure does well is this article
| by metabase: https://medium.com/@metabase/why-we-picked-
| clojure-448bf759d...
|
| I have had the pleasure of contributing to their code since we
| used their product at a previous company I worked at, and I must
| say I am sold on Clojure. Definitely a great language to have in
| your toolbox.
| dgb23 wrote:
| I found one of the perceived weaknesses of Clojure (in this
| article), it being dynamically typed, is a tradeoff rather than a
| pure negative. But it applies that tradeoff differently than
| dynamic languages I know otherwise and that difference is
| qualitative: It enables a truly interactive way of development
| that keeps your mind in the code, while it is running. This is
| why people get addicted to Lisp, Smalltalk and similar languages.
|
| > To understand a program you must become both the machine and
| the program.
|
| - Epigrams in Programming, Alan Perlis
|
| Two of the big advantages of (gradually-) typed languages are
| communication (documentation) and robustness. These can be gained
| back with clojure spec and other fantastic libraries like schema
| and malli. What you get here goes way beyond what a strict,
| static type systems gets you, such as arbitrary predicate
| validation, freely composable schemas, automated instrumentation
| and property testing. You simply do not have that in a static
| world. These are old ideas and I think one of the most notable
| ones would be Eiffel with it's Design by Contract method, where
| you communicate pre-/post-conditions and invariants clearly. It
| speaks to the power of Clojure (and Lisp in general) that those
| are just libraries, not external tools or compiler extensions.
| hota_mazi wrote:
| In 2021, I find it hard to justify using a dynamically typed
| language for any project that exceeds a few hundreds of lines.
| It's not a trade off, it's a net loss.
|
| The current crop of statically typed languages (from the oldest
| ones, e.g. C#, to the more recent ones, e.g. Kotlin and Rust)
| is basically doing everything that dynamically typed languages
| used to have a monopoly on, but on top of that, they offer
| performance, automatic refactorings (pretty much impossible to
| achieve on dynamically typed languages without human
| supervision), fantastic IDE's and debuggability, stellar
| package management (still a nightmare in dynamic land), etc...
| sova wrote:
| I must respectfully disagree with the points you've brought
| up.
| throwaway_fjmr wrote:
| Can you elaborate why? To be honest, I don't have
| experience with large-scale Clojure codebases, but I have
| my fair share working on fairly hefty Python and Perl
| projects, and I tend to think that the parent commenter is
| mostly right. What makes you think they are incorrect?
| uDontKnowMe wrote:
| Not who you are responding to, but the common idea that
| static types are all win and no cost has become very
| popular these days, but isn't true, it's just that the
| benefits of static typing are immediately apparent and
| obvious, but their costs are more diffuse and less
| obvious. I thought this was a pretty good write up on the
| subject that gets at a few of the benefits
| https://lispcast.com/clojure-and-types/
|
| Just to name some of the costs of static types briefly:
|
| * they are very blunt -- they will forbid many perfectly
| valid programs just on the basis that you haven't fit
| your program into the type system's view of how to encode
| invariants. So in a static typing language you are always
| to greater or lesser extent modifying your code away from
| how you could have naturally expressed the functionality
| towards helping the compiler understand it.
|
| * Sometimes this is not such a big change from how you'd
| otherwise write, but other times the challenge of writing
| some code could be virtually completely in the problem of
| how to express your invariants within the type system,
| and it becomes an obsession/game. I've seen this run
| rampant in the Scala world where the complexity of code
| reaches the level of satire.
|
| * Everything you encode via static types is something
| that you would actually have to change your code to allow
| it to change. Maybe this seems obvious, but it has big
| implications against how coupled and fragile your code
| is. Consider in Scala you're parsing a document into a
| static type like. case class Record(
| id: Long, name: String, createTs:
| Instant, tags: Tags, }
| case class Tags( maker: Option[String],
| category: Option[Category], source:
| Option[Source], )
|
| //...
|
| In this example, what happens if there are new fields on
| Records or Tags? Our program can't "pass through" this
| data from one end to an other without knowing about it
| and updating the code to reflect these changes. What if
| there's a new Tag added? That's a refactor+redeploy. What
| if the Category tag adds a new field? refactor+redeply.
| In a language as open and flexible as Clojure, this
| information can pass through your application without
| issue. Clojure programs are able to be less fragile and
| coupled because of this.
|
| * Using dynamic maps to represent data allows you to
| program _generically_ and allows for better code reuse,
| again in a less coupled way than you would be able to
| easily achieve in static types. Consider for instance how
| you would do something like `(select-keys record [:id
| :create-ts])` in Scala. You 'd have to hand-code that
| implementation for every kind of object you want to use
| it on. What about something like updating all updatable
| fields of an object? Again you'll have to hardcode that
| for all objects in scala like case
| class UpdatableRecordFields(name: Option[String], tags:
| Option[Tags]) def update(r: Record,
| updatableFields: UpdatableRecordFields) = { var
| result = r updatableFields.name.foreach(r =
| r.copy(name = _))
| updatableFields.tags.foreach(r = r.copy(tags = _))
| result }
|
| all this is specific code and not reusable! In clojure,
| you can solve this for once and for all!
| (defn update [{:keys [prev-obj new-obj updatable-fields}]
| (merge obj (select-keys new-fields updatable-fields)))
| (update {:prev-obj {:id 1 :name "ross"
| :createTs (now) :tags {:category "Toys"}}
| :new-obj {:name "rachel"} :updatable-fields
| [:name :tags]}) => {:id 1 :name "rachel"
| :createTs (now) :tags {:category "Toys"}}
|
| I think Rich Hickey made this point really well in this
| funny rant https://youtu.be/aSEQfqNYNAc.
|
| Anyways I could go on but have to get back to work,
| cheers!
| codingkoi wrote:
| Your third point about having to encode everything isn't
| quite true. Your example is just brittle in that it
| doesn't allow additional values to show up causing it to
| break when they do. That's not a feature of static type
| systems but how you wrote the code.
|
| This blog post[1] has a good explanation about it, if you
| can forgive the occasional snarkyness that the author
| employs.
|
| In a dynamic system you're still encoding the type of the
| data, just less explicitly than you would in a static
| system and without all the aid the compiler would give
| you to make sure you do it right.
|
| [1]: https://lexi-lambda.github.io/blog/2020/01/19/no-
| dynamic-typ...
| uDontKnowMe wrote:
| I've seen this article and I applaud it for addressing
| the issue thoroughly but I still am not convinced that
| static typing as we know it is as flexible and generic as
| dynamic typing. Let's go at this from an other angle,
| with a thought experiment. I hope you won't find it
| sarcastic or patronizing, just trying to draw an analogy
| here.
|
| So, in statically typed languages, it is not idiomatic to
| pass around heterogeneous dynamic maps, at least in
| application code, like it is in Ruby/Clojure/etc. But one
| analogy we can draw which could drive some intuition for
| static typing enthusiasts is to forget about objects and
| consider lists. It is perfectly familiar to Scala/Java/C#
| programmers to pass around Lists, even though they're
| highly dynamic. So now think about what programming would
| be like if we didn't have dynamic lists, and instead
| whenever you wanted to build a collection, you had to go
| through the same rigamarole that you have to when
| defining a new User/Record/Tags object.
|
| So instead of being able to use fully general `List`
| objects, when you want to create a list, that will be its
| own custom type. So instead of val list =
| List(1,2,3,4)
|
| you'll have to do: case class List4(_0:
| Int, _1: Int, _2: Int, _3: Int) val list =
| List4(1,2,3,4)
|
| This represents what we're trying to do much more
| accurately and type-safely than with dynamic Lists, but
| what is the cost? We can't append to the list, we can't
| `.map(...)` the list, we can't take the sum of the list.
| Well, actually we can! case class
| List5(_0: Int, _1: Int, _2: Int, _3: Int, _4) def
| append(list4: List4, elem: Int): List5 = List5(list4._0,
| list4._1, list4._2, list4._3, elem) def
| map(list4: List4, f: Int => Int): List4 =
| List4(f(list4._0), f(list4._1), f(list4._2), f(list4._3))
| def sum(list4: List4): Int = list4._0 + list4._1 +
| list4._2 + list4._3
|
| So what's the problem? I've shown that the statically
| defined list is can handle the cases that I initially
| thought were missing. In fact, for any such operation you
| are missing from the dynamic list implementation, I can
| come up with a static version which will be much more
| type safe and more explicit on what it expects and what
| it returns.
|
| I think it's obvious what is missing, it's that all this
| code is way too specific, you can't reuse any code from
| List4 in List5, and just a whole host of other problems.
| Well, this is pretty much exactly the same kinds of
| problems that you run into with static typing when you're
| applying it to domain objects like User/Record/Car. It's
| just that we're very used to these limitations, so it
| never really occurs to us what kind of cost we're paying
| for the guarantees we're getting.
|
| That's not to say dynamic typing is right and static
| typing is wrong, but I do think that there really are
| significant costs to static typing and people don't think
| about it.
| codingkoi wrote:
| I'm not sure I follow your analogy. I think the dynamism
| of a list is separate from the type system. I can say I
| have a list of integers but that doesn't limit its size.
|
| I can think of instances where that might be useful and I
| think there's even work being done in that direction in
| things like Idris that I really know very little about.
|
| There are trade offs in everything. I'm definitely a fan
| of dynamic type systems especially things like Lisp and
| Smalltalk where I can interact with the running system as
| I go, and not having to specify types up front helps with
| that. Type inference will get you close to that in a more
| static system, but it can only do so much.
|
| The value I see in static type systems comes from being
| able to rely on the tooling to help me reason about what
| I'm trying to build, especially as it gets larger. I
| think of this as being something like what Doug Englebert
| was pointing at when he talked about augmented
| intelligence.
|
| I use Python at work and while there are tools that can
| do some pretty decent static analysis of it, I find
| myself longing for something like Rust more and more.
|
| Another example I would point to beyond the blog post I
| previously mentioned is Rust's serde library. It totally
| allows you to round trip data while only specifiying the
| parts you care about. I don't think static type systems
| are as static as most like to think. It's more about
| knowns and unknowns and being explicit about them.
| throwaway_fjmr wrote:
| > In a language as open and flexible as Clojure, this
| information can pass through your application without
| issue. Clojure programs are able to be less fragile and
| coupled because of this.
|
| Or this can wreak havoc :) Nothing stops you from writing
| Map<Object, Object> or Map[Any, Any], right?
| uDontKnowMe wrote:
| That's true! But now we'll get into what is possible vs
| what is idiomatic, common, and supported by the
| language/stdlib/tooling/libraries/community. If I
| remember correctly, Rich Hickey did actually do some
| development for the US census, programming sort of in a
| Clojure way but in C#, before creating Clojure. But it
| just looked so alien and was so high-friction that he
| ended up just creating Clojure. As the article I linked
| to points out, "at some point, you're just re-
| implementing Clojure". That being said, it's definitely
| possible, I just have almost never seen anyone program
| like that in Java/Scala.
| ud_visa wrote:
| Let me address your criticism from Scala's point of view
|
| > they are very blunt
|
| I'm more blunt than the complier usually. I really want
| 'clever' programs to be rejected. In rare situations when
| I'm sure I know something the complier doesn't, there are
| escape hatches like type casting or @ignoreVariace
| annotation.
|
| > the problem of how to express your invariants within
| the type system
|
| The decision of where to stop to encode invariants using
| the type system totally depends on a programmer.
| Experience matters here.
|
| > Our program can't "pass through" this data from one end
| to an other
|
| It's a valid point, but can be addressed by passing data
| as tuple (parsedData, originalData).
|
| > What if there's a new Tag added? What if the Category
| tag adds a new field?
|
| If it doesn't require changes in your code, you've
| modelled your domain wrong - tags should be just a
| Map[String, String]. If it does, you have to
| refactor+redeploy anyway.
|
| > What about something like updating all updatable fields
| of an object
|
| I'm not sure what exactly you meant here, but if you want
| to transform object in a boilerplate-free way, macroses
| are the answer. There is even a library for this exact
| purpose: https://scalalandio.github.io/chimney/! C# and
| Java have to resort to reflection, unfortunately.
| outworlder wrote:
| > In 2021, I find it hard to justify using a dynamically
| typed language for any project that exceeds a few hundreds of
| lines. It's not a trade off, it's a net loss.
|
| Only if you are skimping on tests. There's a tradeoff here -
| "dynamically typed" languages generally are way easier to
| write tests for. The expectation is that you will have plenty
| of them.
|
| Given that most language's type systems are horrible (Java
| and C# included) I don't really think it's automatically a
| net gain. Haskell IS definitely a net gain, despite the
| friction. I'd argue that Rust is very positive too.
|
| Performance is not dependent on the type system, it's more
| about language specification (some specs paint compilers into
| a corner) and compiler maturity. Heck, Javascript will smoke
| many statically typed languages and can approach even some C
| implementations(depending on the problem), due to the sheer
| amount of resources that got spent into JS VMs.
|
| Some implementations will allow you to specify type hints
| which accomplish much of the same. Which is something you can
| do on Clojure by the way.
|
| Automatic 'refactorings' is also something that's very
| language dependent. I'd argue that any Lisp-like language is
| way easier for machines to process than most "statically
| typed" languages. IDEs and debugability... have you ever used
| Common Lisp? I'll take a condition system over some IDE UI
| any day. Not to mention, there's less 'refactoring' needed.
|
| Package management is completely unrelated to type systems.
|
| Rust's robust package management has more to do with it being
| a modern implementation than with its type system. They have
| learned from other's mistakes.
|
| Sure, in a _corporate_ setting, where you have little control
| over a project that spans hundreds of people, I think the
| trade-off is skewed towards the most strict implementation
| you can possibly think of. Not only type systems, but
| everything else, down to code standards (one of the reasons
| why I think Golang got popular).
|
| In 2021, I would expect people to keep the distinction
| between languages and their implementations.
| blacktriangle wrote:
| Here's what I've noticed with my tests and dynamic
| languages. I'll get type errors that static typing would
| have caught. However those errors occur in places I was
| missing testing of actual functionality. Had I had the
| functionality tests, then the type error would have been
| picked up by my tests. And had I just had static typing,
| the type system would not have been enough to prove the
| code actually works, so I would have needed tests anyways.
|
| Point being, I don't really buy that a static type system
| saves me any time writing and maintaining tests, because
| type systems are totally unable to express algorithms. And
| with a working test suite (which you will need regardless
| of static vs dynamic) large refactors become just as
| mechanical in dynamic languages as they are in static
| languages.
| tsss wrote:
| > type systems are totally unable to express algorithms
|
| You don't know much about types if you think that.
|
| As for dynamic typing "helping" you to find code that you
| need to write tests for: There are already far more
| sophisticated static analysis tools to measure code
| coverage.
| daxfohl wrote:
| Yeah, I had a fairly large (about a year of solo dev work)
| app that I maintained both Clojure and F# ports of, doing a
| compare and contrast of the various language strengths. One
| day I refactored the F# to be async, a change that affected
| like half the codebase, but was completed pretty mechanically
| via changing the core lines, then following the red
| squigglies until everything compiled again, and it basically
| worked the first time. I then looked at doing the same to the
| Clojure code, poked at it a couple times, and that was pretty
| much the end of the Clojure port.
| dharmaturtle wrote:
| Hey, so my my career path has been C# (many years) -> F#
| (couple years) -> Clojure (3 months). I understand
| multithreading primarily through the lens of async/await,
| and have been having trouble fully grokking the Clojure's
| multithreading. One of the commandments of async/await is
| don't block: https://blog.stephencleary.com/2012/07/dont-
| block-on-async-c...
|
| Which is why the async monad tends to infect everything.
| Clojure, as far as I can tell so far, doesn't support
| anything similar to computation expressions. So I'm
| guessing your "poked at it a couple times" was something
| like calling `pmap` and/or blocking a future? All my
| multithreaded Clojure code quickly blocks the thread... and
| I can't tell if this is idiomatic or if there's a better
| way.
| daxfohl wrote:
| Not even. It was opening it, looking, realizing it would
| take a couple weeks, and going back to F#. I did this a
| couple times before fully giving up.
|
| IIRC/IIUC, Clojure's async support is closer to Go's
| (I've never used go), in the form of explicit channels.
| Though you can wrap that in a monad pretty easily, which
| I did for fun one day
| (https://gist.github.com/daxfohl/5ca4da331901596ae376).
| But neither option was easy to port AFAICT before giving
| up.
|
| Note it's possible that porting async functionality to
| Clojure may have been easier that I thought at the time.
| Maybe adding some channels and having them do their thing
| could have "just worked". I was used to async requiring
| everything above it to be async too. But maybe channels
| don't require that, and you can just plop them in the low
| level code and it all magically works. A very brief
| venture into Go since then has made me wonder about that.
| gnaritas wrote:
| Not remotely true.
| [deleted]
| bcrosby95 wrote:
| I find immutability way more important.
|
| I don't pick Clojure for its dynamic typing, I pick it for
| other reasons. I've tried Haskell but it really doesn't seem
| to mesh with the way I tend to develop a program. But I would
| love to have more static languages with the pervasive
| immutability of Clojure.
| JackMorgan wrote:
| I really like F# for this, it's like Haskell-lite
| joelbluminator wrote:
| It's your opinion though, there's nothing scientific about
| what you're saying. Take mocking for example, in Ruby/Rails
| it's a breeze. In Java you need to invent a dependency
| injection framework (Spring) to do it.
| de_keyboard wrote:
| The best response from the statically-typed world is
| functional programming and explicit dependencies (Haskell,
| OCaml, F#), which makes mocking unnecessary most of the
| time. OOP (Java, C#) is not the true standard for static-
| typing, just the most common one.
| throwaway_fjmr wrote:
| I think you are mistaken. Mocking and DI frameworks are two
| unrelated concepts. There is nothing in Java that forces
| you to use a DI framework, e.g., Spring if you want to use
| mocks during testing.
| joelbluminator wrote:
| Let's say I have a class called User and in it a method
| that says the current time. So User#say_current_time
| which simply accesses the Date class (it takes no
| arguments).
|
| Can you show me how you would mock the current time of
| that method in Java?
|
| It's one line of Ruby/Javascript code to do that.
| lkitching wrote:
| If you want to use DI, in java 8 you could inject a
| java.time.Clock instance in the constructor and provide a
| fixed instance at the required time in your test e.g.
| Instant testNow = ... User u = new
| User(Clock.fixed(testNow, ZoneOffset.UTC));
| u.sayCurrentTime();
|
| although it would be better design to have sayCurrentTime
| take a date parameter instead of depending on an external
| dependency.
| joelbluminator wrote:
| Yes that was my point. You don't need DI or to structure
| your code any differently in Ruby/JS/Python. You just
| mock a method.
| lkitching wrote:
| In my experience the need to mock out individual methods
| like this is an indication that the code is badly
| structured in the first place. The time source is
| effectively a global variable so in this example you'd
| want to pass the time as a parameter to `sayCurrentTime`
| and avoid the need to mock anything in the first place. A
| lot of C#/java codebases do seem to make excessive use of
| mocks and DI in this way though.
| throwaway_fjmr wrote:
| I am assuming this is easier in Ruby because you can
| monkey patch classes?
|
| Mockito in Java has a nifty way of doing this with
| Mockito.mockStatic: @Test public
| void mockTime() throws InterruptedException {
| LocalDateTime fake = LocalDateTime.of(2021, 7, 2, 19, 0,
| 0); try (MockedStatic<LocalDateTime> call =
| Mockito.mockStatic(LocalDateTime.class)) {
| call.when(LocalDateTime::now).thenReturn(fake);
| assertThat(LocalDateTime.now()).isEqualTo(fake);
| Thread.sleep(2_000);
| assertThat(LocalDateTime.now()).isEqualTo(fake);
| } LocalDateTime now = LocalDateTime.now();
| assertThat(now).isAfter(fake);
| assertThat(now).isNotEqualTo(fake); }
|
| Or you can pass a Clock instance and use .now(clock).
| That Clock then can be either a system clock or a fixed
| value.
| joelbluminator wrote:
| > I am assuming this is easier in Ruby because you can
| monkey patch classes?
|
| Yes, that was my point. I see it's possible in Java
| though, hurts my eyes a bit but possible :)
| mypalmike wrote:
| I'll take clean contractual interfaces (aka actual
| principle of least surprise) over "I can globally change
| what time means with one line of code!" on large projects
| every time.
| hota_mazi wrote:
| User mock = mock(User.java)
| when(mock.say_current_time()).thenReturn(someDate)
| joelbluminator wrote:
| OK. first I could be ignorant about Java since I haven't
| touched it in more than a decade. Which library is doing
| that? And also what is mock(User.java) returning - is it
| an actual User instance or a stub? I want a real User
| instance (nothing mocked in it) with just the one method
| mocked.
|
| And again if this is possible I will admit ignorance and
| tip my hat at the Java guys.
| throwaway_fjmr wrote:
| I think what you want is a "spy" (partial mock), not a
| full "mock", but yes, both are possible. You can
| partially mock classes, i.e., specific methods only.
| Syntax is almost the same, instead of mock(User.class)
| you write spy(User.class).
| hota_mazi wrote:
| It's Mockito [1], which has been a standard for a while.
| There are other libraries and they use different
| strategies to provide this kind of functionalities
| (dynamic proxies, bytecode weaving, annotation
| processing, etc...).
|
| [1] https://site.mockito.org/
| joelbluminator wrote:
| And ... is the whole user being mocked or just the
| method?
| vincnetas wrote:
| It creates a stub, but you can also configure it to pass
| any method calls to original implementation. You should
| be tiping your hat i think.
|
| https://javadoc.io/static/org.mockito/mockito-
| core/3.11.2/or...
|
| User mock = mock(User.java)
|
| Should have been
|
| User mock = mock(User.class)
| hota_mazi wrote:
| Ah oops, I've been writing exclusively Kotlin for several
| years, my Java is becoming rusty (no pun intended).
| mmcdermott wrote:
| In theory, I agree, but I don't think that holds terribly
| true in practice.
|
| One of the ideas behind IoC frameworks (which build on
| top of DI) is that you could swap out implementation
| classes. For a great deal of software (and especially in
| cloud-hosted, SaaS style microservice architecture) the
| test stubs are the only other implementations that ever
| get injected.
|
| Most code bases could ditch IoC if Java provided a
| language-level construct, even if that construct were
| only for the test harness.
| hota_mazi wrote:
| The fact that there are such libraries in existence means
| that there is no pain associated to this particular
| activity. Not only do you get great mocking frameworks,
| they are actually very robust and benefit from static
| types.
|
| Mocking dynamically typed languages is monkey patching,
| something that the industry has been moving away from for
| more than a decade. And for good reasons.
| joelbluminator wrote:
| > The fact that there are such libraries in existence
| means that there is no pain associated to this particular
| activity
|
| I can say the same about Rails + RSpec. It exists
| therefore it's good.
|
| > Mocking dynamically typed languages is monkey patching,
| something that the industry has been moving away
|
| That's a reach. There are millions of
| javascript/python/php/ruby/elixir devs that don't use
| types or annotations. They mock. "The industry" isn't one
| cohesive thing.
| [deleted]
| geokon wrote:
| I've admittedly not played with spec, but can't you solve
| documenting interfaces by defining `defrecord`s ? You rarely
| really care about the actual types involved. You just want to
| know which fields you either need to provide or will recieve
| roenxi wrote:
| Spec will give you stronger feedback than a docstring or
| function signature. It can tell you (in code terms, with a
| testable predicate) if a call to an interface wouldn't make
| sense.
|
| Eg, spec can warn you when an argument doesn't make sense
| relative to the value of a second argument. Eg, with
| something like (modify-inventory {:shoes 2} :shoes -3) spec
| could pick up that you are about to subtract 3 from 2 and
| have negative shoes (impossible!) well before the function is
| called - so you can test elsewhere in the code using spec
| without having to call modify-inventory or implement
| specialist checking methods. And a library author can pass
| that information up the chain without clear English
| documentation and using only standard parts of the language.
|
| You can't do that with defrecord, but it is effectively a
| form of documentation about how the arguments interact.
| modernerd wrote:
| Does the spec logic typically live inside the modify-
| inventory function, or elsewhere? If elsewhere, what
| triggers it before the function is called?
| teataster wrote:
| There is very little spec logic. It looks a lot like type
| declarations in typed languages.
|
| It's usually outside the scope of functions, since you
| are likely going to want to reuse those declarations. For
| example, you can use spec to generate test cases for
| something like quick-check.
|
| You can add pre and post conditions to clojure function's
| metadata that test wether the spec complies with the
| function's input/output.
| k__ wrote:
| I think, the big issue with dynamic typing in popular languages
| like PHP and JavaScript are the automatic conversions.
| robertlagrant wrote:
| This is a consequence of weak typing rather than dynamic
| typing. I appreciate that these are not precise terms, but
| being able to change something's type (dynamic) is different
| to the language just doing strange things when you combine
| types (weak).
| dgb23 wrote:
| You mean implicit type conversions? That's a thing you can
| get somewhat used to. But it throws off beginners and can
| introduce super weird bugs, because they hide bugs in weird
| ways, even if you are more experienced. Yes, I find strong
| typing strictly better than weak typing.
|
| An even better example of this would be Excel, the horror
| stories are almost incredible.
|
| So even if your environment is dynamic, you want clarity when
| you made a mistake. Handling errors gracefully and hiding
| them are very different things. The optimal in a dynamic
| world is to facilitate reasoning while not restricting
| expression.
| dangerbird2 wrote:
| It's always worth reminding folks that weak typing and
| implicit conversions can plague statically typed languages.
| C's implicit pointer array-to-pointer and pointer-type
| conversions are a major source of bugs for beginner and
| experienced programmers alike.
| elwell wrote:
| Which is less of a concern considering Clojure's focus on
| immutability.
| k__ wrote:
| That's what I meant.
|
| As far as I know, some dynamic languages like Python don't
| have that issue.
| vnorilo wrote:
| Agreed. I feel Lisps and SmallTalk are dynamic done right. I
| think the other language features that you use also influence
| the value from dynamic or static types. For OOP style, static
| types are a huge asset for refactoring and laying our
| architecture. On the other hand, immutable data and stateless
| functions (as idiomatic in clojure) make them less necessary,
| and also work great together with interactive development.
| pjmlp wrote:
| Not only that, Smalltalk and Lisps are languages designed with
| developer experience as part of the language.
|
| You just don't get an interpreter/compiler and have to sort
| everything else by yourself, no, there is a full stack
| experience and development environment.
| c-cube wrote:
| Except, of course, that specs are only tested correct, not
| proven correct like types would be. Types (in a reasonable
| static type system, not, say, C) are never wrong. In addition,
| specs do not compose, do they ? If you call a function g in a
| function f, there is no automatic check that their specs align.
| undershirt wrote:
| Yeah, and I think this is obvious, but it certainly depends
| on the _origin_ of the data being checked. We can prove the
| structure of "allowed" data ahead of time if we want
| guarantees on what's possible inside our program. We also
| want a facility to check data encountered by the running
| program (i.e. from the user or another program.) which of
| course we can't know ahead of time.
|
| It is a design decision to be able to build a clojure system
| interactively while it is running, so a runtime type checker
| is a way for the developer to give up the safety of type
| constraints for this purpose--by using the same facility we
| already need in the real world, a way to check the structure
| of data we can't anticipate.
| travisjungroth wrote:
| > Types (in a reasonable static type system, not, say, C) are
| never wrong.
|
| Oh man. This is the fundamental disagreement. Sure, you can
| have a type system that is never wrong in its own little
| world. But, that's not the problem. A lot of us are making a
| living mapping real world problems into software solutions.
| If that mapping is messed up (and it always is to some
| degree) then the formal correctness of the type system
| doesn't matter _at all_. It 's like you got the wrong answer
| really, really right.
| lolinder wrote:
| > A lot of us are making a living mapping real world
| problems into software solutions. If that mapping is messed
| up (and it always is to some degree) then the formal
| correctness of the type system doesn't matter at all.
|
| If I'm understanding you correctly, you're saying
| statically typed language can't protect against design
| flaws, only implementation flaws. But implementation flaws
| are common, and statically typed languages _do_ help to
| avoid those.
| c-cube wrote:
| I'm not saying types always model your problem properly!
| That's not even well specified. I'm saying that "x has type
| foo" is never wrong if the program typechecks properly.
| That's totally different, and it means that you can rely on
| type annotations as being correct, up-to-date
| documentation. You can also trust that functions are never
| applied to the wrong number of arguments, or the wrong
| types; my point is that this guarantees more, in a less
| expressive way, than specs.
| travisjungroth wrote:
| You can statically analyze specs and check them at
| runtime if you want.
| cle wrote:
| > Except, of course, that specs are only tested correct, not
| proven correct like types would be.
|
| Yes this is the fundamental tradeoff. Specs et al are
| undoubtedly more flexible and expressive than static type
| systems, at the expense of some configurable error tolerance.
| I don't think one approach is generally better than the
| other, it's a question of tradeoffs between constraint
| complexity and confidence bounds.
| dgb23 wrote:
| Yes, I think that is one of the big weaknesses of it. You can
| write specs that make no sense and it will just let you. So
| far there is also no way to automatically check whether you
| are strengthening a guarantee or weaken your assumptions
| relative to a previous spec. In a perfect world we would have
| this in my opinion.
| travv0 wrote:
| The very first property testing library was written in Haskell,
| as far as I know.
| blacktriangle wrote:
| I feel like there's a missing axis in the static/dynamic
| debate: the language's information model.
|
| In an OOP language, types are hugely important, because the
| types let you know the object's ad-hoc API. OOP types are
| incredibly complicated.
|
| In lisps, and Clojure in particular, your information model is
| scalars, lists, and maps. These are fully generic structures
| whose API is the standard Clojure lib. This means that its both
| far easier to keep the flow of data through your program in
| your head.
|
| This gives you a 2x2 matrix to sort languages into, static vs
| dynamic, and OOP vs value based.
|
| * OOP x static works thanks to awesome IDE tooling enabled by
| static typing
|
| * value x static works due to powerful type systems
|
| * value x dynamic works due to powerful generic APIs
|
| * OOP x dynamic is a dumpster fire of trying to figure out what
| object you're dealing with at any given time (looking right at
| you Python and Ruby)
| scotty79 wrote:
| > ... such as arbitrary predicate validation, freely composable
| schemas, automated instrumentation and property testing ...
|
| Why static typing makes those things impossible?
| dgb23 wrote:
| They don't make these impossible, they typically just don't
| let you express these within the type system and they
| typically don't let you _not_ specify your types.
|
| I should have made clear that I'm emphasizing the advantages
| of being dynamic to describe and check the shape of your data
| to the degree of your choosing. Static typing is very
| powerful and useful, but writing dynamic code interactively
| is not just "woopdiedoo" is kind of the point I wanted to
| make without being overzealous/ignorant.
| scotty79 wrote:
| I think TypeScript is best of both worlds.
|
| Typesystem strong enough to express dynamic language and
| completely optional wherever you want.
| thinkharderdev wrote:
| That largely depends on the type system. Languages like
| Haskell and Scala which have much more powerful type
| systems than C/Java/Go/etc absolutely do allow you to do
| those sorts of things. It is a bit harder to wrap your head
| around to be sure and there are some rough edges, but once
| you get the hang of it you can get the benefits of static
| typing with the flexibility of dynamic typing. See
| https://github.com/milessabin/shapeless or a project that
| I've been working on a lot lately
| https://github.com/zio/zio-schema.
| dharmaturtle wrote:
| > What you get here goes way beyond what a strict, static type
| systems gets you, such as arbitrary predicate validation,
|
| Is this refinement types, which most static languages provide?
| https://en.wikipedia.org/wiki/Refinement_type
|
| > freely composable schemas,
|
| My understanding is that you can compose types (and objects)
| https://en.wikipedia.org/wiki/Object_composition
|
| I'm assuming that types are isomorphic with schemas for the
| purposes of this discussion.
|
| > automated instrumentation
|
| I know that C# and F# support automated
| instrumentation/middleware.
|
| > and property testing. You simply do not have that in a static
| world.
|
| QuickCheck has entered the chat:
| https://en.wikipedia.org/wiki/QuickCheck
| CraigJPerry wrote:
| >> Is this refinement types
|
| Well it does include that kind of behaviour but it's quite a
| bit more than just that. E.g. you could express something
| like "the parameter must be a date within the next 5 business
| days" - there's no static restriction. I'm not necesarily
| saying you should but just to give an illustrative example
| that there's less restrictions on your freedom to express
| what you need than in a static system.
|
| >> types are isomorphic with schemas
|
| I don't think that's a good way to think of this, you're
| imagining a rigid 1:1 tie of data and spec yet i could swap
| out your spec for my spec so that would be 1:n but those
| specs may make sense to compose in other data use cases so
| really it's m:n rather than 1:1
| dharmaturtle wrote:
| > E.g. you could express something like "the parameter must
| be a date within the next 5 business days" - there's no
| static restriction
|
| Hm, I don't follow. If I were to write this in F#, there
| would be a type `Within5BusinessDays` with a private
| constructor that exposes one function/method `tryCreate`
| which returns a discriminated union: either an `Ok` of the
| `Within5BusinessDays` type, or an `Error` type with some
| error message. Once I have the type, I can then compose it
| with whatever and send it wherever and since F# records are
| immutable, I won't have to worry about invariants not
| holding. And since it's a type, I have the compiler/type
| system on my side to help with correctness.
|
| (Side note, this is a bad example since the type can become
| invalid after literally 1 second... but since Clojure has
| the same problem I'm just running with it.)
|
| I'm still learning Clojure (only a few months into it), but
| if I were to to write a spec, I'd have to specify what to
| do do if the spec failed to conform - same as returning the
| `Error` case in F#.
|
| > i could swap out your spec for my spec so that would be
| 1:n but those specs may make sense to compose in other data
| use cases so really it's m:n rather than 1:1
|
| Sorry, but I'm still not following - I believe you can do
| the same with types, especially if the type system support
| generics.
| CraigJPerry wrote:
| > If I were to write this in F#, there would be a type
| `Within5BusinessDays`
|
| That's not really the same thing - it'sa valid
| alternative approach but you've lost the benefits of a
| simple date - from (de)serialisation to the rich support
| for simple date types in libraries and other functions,
| the simple at-a-glance understanding that future readers
| could enjoy. Now the concept of date has been complected
| with some other niche concern.
|
| > the type can become invalid after literally 1 second
|
| Every system I've ever seen that has the concept of a
| business date strictly doesn't derive it from wall clock
| date. E.g. it's common that business date would be rolled
| at some convenient time (and most often not midnight) so
| you'd be free to ensure no impacts possible from the date
| roll.
|
| >> I believe you can do the same with types, especially
| if the type system support generics
|
| You can do something similar but you'll need to change
| the system's code.
|
| It would be almost like gradual typing, except you could
| further choose to turn it off or to substitute your own
| types / schema without making changes to the system /
| code.
|
| It's quite a lot more flexible.
|
| (Apols for slow reply - i 1up'd your reply earlier when i
| saw it but couldn't reply then)
| dharmaturtle wrote:
| > but you've lost the benefits of a simple date
|
| I see what you mean - thanks!
|
| > you could further choose to turn it off or to
| substitute your own types / schema without making changes
| to the system / code
|
| This is still unclear to me. How can you make changes
| (turning off gradual typing/substituting your own schema)
| without making changes to code?
| dgb23 wrote:
| Right! I made the dumb, typical error to write: "You simply
| do not have that in a static world." When I should have
| written: "This type of expressiveness is not available in
| mainstream statically typed languages".
|
| With "freely composable" I mean that you can program with
| these schemas as they are just data structures and you only
| specify the things you want to specify. Both advantage and
| the disadvantage is that this is dynamic.
| dharmaturtle wrote:
| Ah, well if you're going to shit on Go/Java/C#/C++ I won't
| stop you :)
| scotty79 wrote:
| I tried to use Clojure but what put me of was that simple
| mistakes like missing argument or wrongly closed bracket didn't
| alert me until I tried running the program and then gave me just
| some java stack spat out by jvm running Clojure compiler on my
| program.
|
| It didn't feel like a first class experience.
| capableweb wrote:
| > didn't alert me until I tried running the program
|
| That's because that's not how Clojure developers normally work.
| You don't do changes and then "run the program". You start your
| REPL and send expressions from your editor to the REPL after
| you've made a change you're not sure about. So you'd discover
| the missing argument when you call the function, directly after
| writing it.
| scotty79 wrote:
| Interesting. How exactly that looks? Do you have files opened
| in your editor, change them then go into previously opened
| repl, and just call the functions and the new version of
| those function runs?
| finalfantasia wrote:
| Thanks to the dynamic nature of Clojure programs,
| experienced Clojure developers use the REPL-driven
| development workflow as demonstrated in this video [1].
|
| [1] https://youtu.be/gIoadGfm5T8
| scotty79 wrote:
| From what I understand, instead of writing the file and
| running the file you write separate statements in the
| file and evaluate each of them in the repl (like with "Do
| it" in Smalltalk).
|
| So what you get, after running the file afterwards from
| clean state might be different than the result of your
| selective separate manual evaluations.
|
| This looks like exactly the opposite of the F5 workflow
| in the browser where you can run your program from clean
| state with single keypress.
|
| I haven't watched the video till the end though maybe
| there's a single key that restarts the repl and runs the
| files from clean state here too.
|
| At first glance you could have the same workflow with JS,
| but there's not much need for it because JS VMs restart
| very quickly and also you'd need to code in JS in very
| particular style, avoiding passing function and class
| "pointers" around and avoid keeping them in variables. I
| guess clojure just doesn't do that very often and just
| refers to functions through their global identifiers, and
| if that's not enough, even through symbols (like passing
| the #'app in this video instead of just app).
| uDontKnowMe wrote:
| That's right. You typically would have your text editor/ide
| open, and the process you're developing would expose a repl
| port which your editor can connect to. As you edit the
| source code, that will automatically update the code
| running in the process you're debugging. See this demo of
| developing a ClojureScript React Native mobile app
| published yesterday: https://youtu.be/3HxVMGaiZbc?t=1724
| [deleted]
| pron wrote:
| > We tried VisualVM but since Clojure memory consists mostly of
| primitives (Strings, Integers etc) it was very hard to understand
| which data of the application is being accumulated and why.
|
| You should try deeper profiling tools like JFR+JMC
| (http://jdk.java.net/jmc/8/) and MAT
| (https://www.eclipse.org/mat/).
| gavinray wrote:
| I was going to suggest this -- inside of VisualVM, you can
| right-click a process and then press "Start JFR"
|
| Then wait a bit, right click it again, and select "Dump JFR"
|
| What you get is a Flight Record dump that contains profiling
| information you can view that's more comprehensive than any
| language I've ever seen.
|
| I used this for the first time the other day and felt like my
| life has been changed.
|
| Specifically, if you want to see where the application is
| spending it's time and in what callstacks, you can use the CPU
| profiling and expand the threads -- they contain callstacks
| with timing
|
| There's some screenshots in an issue I filed here showing this
| if anyone if curious what it looks like:
|
| https://github.com/redhat-developer/vscode-java/issues/2049
|
| Thanks Oracle.
| user3939382 wrote:
| Walmart Labs was a step in this direction.. but we need some big
| companies to standardize around Clojure to jumpstart the
| ecosystem of knowledge, libraries, talent, etc. I've spoken to
| engineering hiring managers at fairly big companies and they're
| not willing to shift to a niche language based only on technical
| merits but without a strong ecosystem.
|
| If we don't get some big companies to take on this roll the
| language is going nowhere.
|
| I'm saying this because I'm a huge fan of Clojure (as a syntax
| and language, not crazy about the runtime characteristics) and I
| hope I get the opportunity to use it.
| iLemming wrote:
| > If we don't get some big companies to take on this roll
|
| - Cisco - has built their entire integrated security platform
| on Clojure
|
| - Walmart Labs and Sam's club - have some big projects in
| Clojure
|
| - Apple - something related to the payment system
|
| - Netflix and Amazon, afaik they use Clojure as well
|
| even NASA uses Clojure.
|
| I think the language "is going somewhere"...
| user3939382 wrote:
| role* lol we made the same mistake. There is some adoption to
| be sure. But look at Google Trends for clojure.
| ivanech wrote:
| I started working professionally with Clojure earlier this year
| and this article rings true. I think the article leaves out a
| fourth downside to running on the JVM: cryptic stack traces.
| Clojure will often throw Java errors when you do something wrong
| in Clojure. It's a bit of a pain to reason about what part of
| your Clojure code this Java error relates to, especially when
| just starting out.
| alaq wrote:
| How did you make the switch? Were you already working for the
| same company? Did you already know Clojure, from open source,
| or side projects?
| ivanech wrote:
| I work at Ladder [0], and almost everything is done in
| Clojure/ClojureScript here. I had no previous experience in
| Clojure - Ladder ramps you if you haven't used it before. My
| interview was in Python. We're currently hiring senior
| engineers, no Clojure experience necessary [1].
|
| [0] https://www.ladderlife.com/
|
| [1] https://boards.greenhouse.io/ladder33/jobs/2436386
| finalfantasia wrote:
| To be fair, this is not unique to Clojure. You need to deal
| with stack traces no matter what as long as you're using any
| programming language that targets the JVM (even statically
| type-checked languages like Scala). There are some great
| articles [1][2] that discuss various simple techniques helpful
| for debugging and dealing with stack traces.
|
| [1] https://eli.thegreenplace.net/2017/notes-on-debugging-
| clojur...
|
| [2] https://cognitect.com/blog/2017/6/5/repl-debugging-no-
| stackt...
| rockostrich wrote:
| I've never really had a problem with stack traces in Scala.
| Every once in a while you hit a cryptic one that's buried in
| Java library code, but for the most part they're runtime
| errors that are due to incompletely tested code or some kind
| of handled error with a very specific message.
| aliswe wrote:
| > ... and the question regarding choosing Clojure as our main
| programming language rose over and over again
|
| If I find myself having to repeat myself justifying a certain
| decision time and time again, it's an indicator that the decision
| needs to be revised to be something which is a more intuitive fit
| for the organization.
| outworlder wrote:
| That's not a good indication that the decision was or was not
| correct. Only that it currently runs against whatever the
| established practice is. Sometimes "the way things have always
| been done" is just wrong.
|
| This is unlikely to be the case in the choice of programming
| languages. Some may be a bad fit, some may have ecosystems that
| are unpleasant to use, but it's generally not the biggest
| problem an organization will have.
| lostcolony wrote:
| Not really; it's like stoplights. You're going to be
| interrupted and therefore notice the red lights, and just sail
| easily through and thus not notice the green lights. Likewise,
| you're going to notice the pain points, but need to take a
| minute to reflect to notice the benefits.
|
| Really, if repeating the same justifications convinces people,
| then the problem isn't the justifications.
| joelbluminator wrote:
| I donno why you're being downvoted, it's a questionable
| decision and probably the company would have been better off
| with Python/PHP/Node. Hiring and onboarding are extremely
| important for a startup. You know what else? Finding answers to
| common questions on Google/Stackoverflow; I am now working with
| Ember and can tell you guys you take a 50% productivity hit by
| using a tool that's obscure on Google. Sure once you become
| super familiar with a tool that matters less, but that takes
| time. Much more time. React/Angular may be an inferior tool to
| Ember but the fact that you can get answers to almost any
| question is priceless. The community size is super important.
| The frameworks are super important (is there a Closure
| equivalent to Rails/Django/Laravel in community size, in battle
| testedness? I really doubt it).
|
| That being said, I salute these brave companies for sticking to
| these obscure languages. Do we want to live in a world where
| there's only 3 languages to do everything? Even 10 sounds
| boring. Hell, even a fantastic tool like Ruby is considered
| Niche in certain parts of the world. I don't want a world
| without Ruby so I don't want a world without Closure.
| yakshaving_jgt wrote:
| Alternatively, you could document the thought process that lead
| up to the decision and you can point the unenlightened to the
| documentation instead of having to repeat yourself.
___________________________________________________________________
(page generated 2021-08-02 23:01 UTC)