After 14 years of software engineering experience, I started experimenting a little and reading about Lisps and Clojure. I got a feeling that this is the right way of programming. Before I dig deeper however, I wanted to ask if it is possible to setup typing and an editor/ide so that I get immediate type hints, while typing? Just like one gets with TypeScript in VSCode. I intend to do web development in Clojure/ClojureScript. (utilisng Reagent and cljs node)
What exactly do you mean by "type hints"?
When I pass an argument of incorrect type to a function, I would like to have it underlined in red. With an optional pop up indicating the type error/mismatch. I would like to also have auto completion based on the specified types. Type inference supporting both above would also be nice.
Highlighting incorrect types - only in some very narrow cases with external tooling like clj-kondo. I think Calva has it built-in. Auto-completion - only when you're doing interop, and also in very limited cases. Cursive is better here. But mostly in interop with Java, not with JS. Type inference - maybe Clojure Typed, but I haven't used it myself so no actual idea. Clojure is a dynamically typed language. It's very uncommon to put types around everything. Most of your values will be plain vectors/maps/seqs anyway.
Clojure is not a typed language; from a type theory perspective, it's "untyped", or has a single "Value" type. Which means what you're asking for here is unfortunately not possible. Clojurists typically think what they get in return for giving that up is worth it, but you'll have to make your own judgment on that. The way we work around this issue is with tests and a REPL. It's a lot better than it sounds, though, because (iodiomatic) Clojure encourages using a very small number of types and not defining new ones. So confusion on types is actually pretty rare in practice. It's a pretty big mental shift, though, if you're used to nominal type creation as a default way of organizing the world.
What about being informed by the editor/ide that a supplied map to a function does not match what the function expects, as well as autocomplete for this?
That's not supported. As gaverhae said, usually you write a bunch of code leaning on your mental model and pretty much immediately and in an almost frictionless manner run it in a REPL with a single keystroke. The functions tend to stay relatively small, focused around specific bits of the data. So suppose you're doing some event counting and you care about event types and dates - well, you use just those two fields then and completely ignore the rest. None of the type hinting will help you here since there's nothing to help you with, it's already trivial. If there are some huge maps being passed around, there could be a spec/schema somewhere that you consult to see what keys it can have and what properties the corresponding values must satisfy (not quite "types" as TS sees them - arbitrary predicates with the most commonly used things being easily expressed with a single token).
Taking perhaps a little bit of a step back. What does this type-checking and IDE integration give you, really? From my perspective, it gives you three things:
• Discoverability. If you don't know what methods TypeX has, being able to type value_of_type_x. and see what the IDE suggests is very useful. This is very low value in most Clojure code because we have about 11 types we all regularly use, they're the same types, and their operations are covered by the standard library, and you very quickly get to just know them.
• Typo avoidance. Using auto-complete means no typo. That's nice. What "static typist" typically fear on this point is that they'll have a production bug because of a typo in a keyword. Clojure goes a bit further than most dynamic languages here as all of your symbols are checked for existence at compile-time; for keywords, well, you have to rely on tests and REPL-driven development. I can't give you a guarantee, but I can tell you that in practice it's not a problem I've seen.
• Time saving. Using auto-completion is faster than typing things out. That's imo fairly minor, but there are still various ways your IDE can help you in Clojure, as most core functions are short, and non-core functions you'll be using come from namespaces you've imported. You can still have auto-complete on namespace prefixes, and it's relatively easy in most editors to set up auto-completion of keywords based on which keywords already exist in the current document (also helping a bit with the typo issue).
Going a bit further than what you're asking for, there are genuine cases where types are hard to follow, because you have a complex combination of them. Is this function returning a list of lists of lists, or just a lis of lists? Is this vector meant as a collection, or a cheap way to group values together? Ideally, you learn to structure your data and your code in such a way that these do not happen (e.g. always use a map for grouping attributes, instead of positional vectors; prefer flat maps over deeply nested structures, etc.), but sometimes it does happen and needs a bit of REPL-driven debuggiing.
(In addition to everything above) I'm wary to recommend too many third-party libraries to a newcomer, but if you really really feel you still want something like concrete type-scripty data-types after some soul-searching, there is Metosin's Malli library, it has some setups that'll let you do something like what your talking about wrt 'type-checking' (but not the autocomplete thing). I would say try to live without it first, but Malli is a very solid library, especially for bigger projects with complicated data-structures.
I would strongly encourage you to view malli as a schema-checking function for incoming external data, rather than a type-checking system for your own code, but it is an option.
Take the plunge man. There are a lot of resources on the nuts and bolts—in addition to those I recommend https://leanpub.com/elementsofclojure which I think 1) is great and 2) speaks to your concern with typechecking
Please take my contribution to this conversation with a grain of salt as I'm new to Clojure and a hobbyist programmer at best. Eric Normand was on Richard Feldman's podcast, Software Unscripted in April of 2023 discussing types and Clojure. The title is "Haskell and Clojure in Production" the basic question was: Why doesn't Eric get more type errors in Clojure? Eric Normand's name seems to come up often in the Clojure community. He's written a few books and offers a Clojure course as well. Here's the transcript of that conversation. You can play the podcast from this page too. https://ericnormand.me/speaking/software-unscripted
We should be able to infer a bit more than what we do today, but so far at least Calva doesn’t have any of this this. But there are thousands of us using Calva and Clojure daily and I think most of us are super happy. 😀 (I’m biased, as the creator of Calva) As for reagent. You may want to at least have a look at #replicant React-ish without React. There’s some quite amazing videos on YouTube to get the urge to try it!
This: https://www.youtube.com/watch?v=AGTDfXKGvNI is a very good introductory overview of what it may feel like to work on a frontend with replicant. It's not just replicant itself; Christian has built a pretty nice ecosystem around it. More videos are available on Christian's YouTube channel: https://www.youtube.com/@replicant-clj. I've also found the official docs pretty well done: https://replicant.fun.
Clojure programming is like Back to the Future: "Were were going you wont need (to specify) types" Clojure LSP can be used with editors to provide navigation, autocomplete and refactor tools. A Clojure REPL is the most powerful tool, as you can run any piece of the code and get feedback. Therefore it is easy call a function and see what it returns, or pull apart the code in a function and see what that does. As mentioned, maps and other data structures are typically used as arguments. Specifications can be defined for data and function arguments using clojure.spec or mali. Specifications are included in error reports from the repl, so evaluating code gives instant feedback. Test runners like Kaocha can check also check if functions are called correctly (and check return values are of expected types) If using Interop with Java libraries, there may be a case for specifying some type hints for performance or ensuring values are a specific type.
Thank you everyone for your time and input
metosin/malli appears to provide optional type checking that I would like to have in order to pursue clojure/script further. At the moment I need to run
(require '[malli.clj-kondo :as mc])
(-> (mc/collect *ns*) (mc/linter-config))
(mc/emit!)
in order for kondo-clj to pick it up.
Is there some sort of default way of automating this? So I can just type and have Calva/kondo-clj automatically recognise the occasional type annotations I make?mm, #malli might have something for you, but otherwise, my only use of it has been via the "`dev-mode`"
Sorry, I don't understand what exactly you mean by >my only use of it has been via the "`dev-mode`"
https://github.com/metosin/malli#development-mode <- this here
Hello, I am still struggling to have a separate websocket server and client working. My overall code so far is as follows:
(ns tstwebsock.WebsocketServe
(:require [clojure.core.async :as async
:refer [<! <!! >!! close! chan
mult tap untap]]
[org.httpkit.server :as http]
[org.httpkit.client :as httpcl]
[hato.websocket :as ws]
))
;; async channel for sending data to websockets
(def livedata_chan (chan 20))
(def livedata_mux_chan (mult livedata_chan))
;; websocket server and routes
(def app (fn [req]
(case (:uri req)
"/ws" (http/as-channel
req
{:on-receive (fn [ch message] (http/send! ch "Echoing back : " message))
:on-close (fn [ch status-code]
(untap livedata_mux_chan ch)
(println "Closed with code: " status-code))
:on-open (fn [ch]
(println "Got a connection!")
(tap livedata_mux_chan ch)
(http/send! ch (str {:status "connected"})))})
"/home" {:status 200
:headers {"Content-Type" "text/html"}
:body "hello HTTP!"})))
(defonce server (atom nil))
(defn start! []
(when-not @server
(reset! server (http/run-server #'app {:port 8080}))))
(defn stop! []
(when-not (nil? @server)
(@server :timeout 100)
(reset! server nil)))
(start!)
(stop!)
;; ============================================ websocket client =============================================
(def soc @(ws/websocket ""
{:on-open (fn [w]
(ws/send! w "Connect")) ;;
:on-message (fn [w msg last?]
(let [ret (.toString msg)]
(prn ret)))
:on-error (fn [w err]
(prn "Error from Socket : " err))
:on-close (fn [w status reason]
(prn (str "Websocket Closed! with reason " reason " and status : " status)))}))
(ws/close! soc)
On executing all the code without closing the websocket in the last line I get the following output
clj꞉꞉>
; Got a connection!
; Closed with code: ":server-close{
; :status \"connected\"}"
; "Echoing back : "
; "Websocket Closed! with reason and status : 1000"
The question is why is the websocket connection not staying open and immediately closing? Is this because I have something wrong in my handlers? There is so few examples available about these aspects online on websockets. I really appreciate all the help!For future reference, please attach long code fragments as snippets. They have syntax highlighting and are collapsible, making it easier to scroll to the past messages.
As for the issue - take a close look at this form: (http/send! ch "Echoing back : " message).
And check out the docstring of the function.
Aaah ok I got it. On changing the line you highlighted to
:on-receive (fn [ch message] (http/send! ch (str "Echoing back : " message)))
the error disappeared and the connection stays open! Thanks @p-himik for the insight. I've been at it for 2 days on this now. I really appreciate it!