HTTP Fx for Re-Frame on React Native

Once you’ve grokked the Re-Frame philosophy for building truly reactive applications on React in ClojureScript, you will want to apply it everywhere. One obvious and ubiquitous application of effects is asynchronous HTTP requests, and of course there is an officially sanctioned library for handling these. One problem: it relies on an AJAX library that only works with the browser XHR implementation. So what can we do in React Native-based Clojurescript apps?

It’s actually very simple, and can be accomplished in about ten lines of code. We’ll rely on cljs-http, a core.async-based HTTP library.

(ns my-app.events
  (:require [cljs-http.client :as http]
            [cljs.core.async :refer [go <!]]
            [re-frame-core :refer [reg-fx dispatch]]))

(reg-fx
 :http
 (fn [{:keys [url method opts on-success on-failure]}]
   (go
     (let [http-fn (case method
                     :post http/post :get http/get
                     :put http/put :delete http/delete)
           res     (<! (http-fn url opts))
           {:keys [status success body]} res]
       (if success
         (dispatch (conj on-success body))
         (dispatch (conj on-failure body)))))))

The opts param here is a map that will handle everything that cljs-http takes as input, like :headers, or :multipart-params. That documentation can be found in the cljs-http README.

Here’s an example of how you might use it:

(reg-event-fx
  :update-password
  (fn [_ [_ email old-pass new-pass]]
    {:http {:method :post
            :url "https://my-service.example.com/update-password"
            :opts {:edn-params {:user/email email
                                :user/old-password old-pass
                                :user/new-password new-pass}}
            :on-success [:save-user]
            :on-failure [:alert-failure]}}))

You’ll want to have registered the :save-user and :alert-failure events before using them, of course.

Hopefully this small event function has been helpful. It’s short enough that I don’t think it rises to the level of being a library instead of a blog post, and in fact I think most people will want to customize which keys they want to handle. I personally have replaced opts with just edn-params and headers as those are all I use. Feel free to reach out over twitter or in #cljsrn on Clojurians Slack with questions.

Permalink

PurelyFunctional.tv Newsletter 332: Tool: jEnv

Issue 332 – June 24, 2019 · Archives · Subscribe

JVM Tool 🔨

jEnv

The JVM has always been a very stable platform, and releases were seldom. You could get away with running a single version of Java for years. That’s no longer the case. Java 9 introduced some breaking changes, so many people are running Java 8 in production, past the end of life date. And Oracle now releases new JDK versions every 6 months.

I write software that has to run on my customers’ machines, and I can’t really control what version of the JDK they have. Recently, I’ve found myself installing and switching between multiple JDK builds so I could test out all of the combinations. Somewhere in there, I discovered a tool called jEnv. It lets you switch between different JDKs.

You can set the JDK globally, to enable a default JDK. You can set the JDK for a specific directory, for a per-project JDK. And you can set the JDK for the running shell instance, if you need to test a project in a different JDK from the per-project setting. It’s quite nice to have the ability to quickly switch between your installed JDKs.

Clojure Media 🍿

A couple of great Clojure talks have popped up on my radar recently.

Reitit, the Ancient Art of Routing by Tommi Reiman was a cool romp through the internals of Reitit, Metosin’s routing library. They’ve spent the time to make it super fast. The numbers he was showing were competitive with go’s router, which is very impressive. He was also encouraging that Clojure could create a very fast stack from curated libraries.

Crawling Inside the Tauntaun by James Cash was an adventure deep into the bowls of Clojure. It was nice to hear some of the hard truths of debugging.

There were lots of other great talks at Clojure/north. Check them out.

Brain skill 😎

Make it social.

It’s funny how differently we think about people vs about symbols, even when the problems are equivalent. Having trouble with a hairy conditional? Instead of doing the logical negation and conjunctions in your head, try to imagine a person saying it. If the person is telling the truth, it’s true. If they are lying, it’s false. It’s crazy how that works, but it does. Our brains must have logical engines that only work to check people’s stories, but turn off if we’re trying to actually do logic!

Clojure Challenge 🤔

Last week’s challenge

The puzzle in Issue 331 was to come up with ways to generate sorted lists.

I got some great submissions. You can check them out here.

Some notable ones:

  1. Generate a random initial number and a sequence of positive numbers. As you add the positive numbers to the initial number, it generates a sequence of numbers that only get bigger.
  2. Similarly, you could generate a random sorted sequence of strings by appending a random character to the end of the last element.
  3. Apply any monotonic function to an already sorted list, such as the sequence of natural numbers. For example, map square root over (range 100). It’s not exactly random, but you could select a random slice from that.

I think the creative juices flowed nicely on this one.

This week’s challenge

anagrams

Two words are anagrams of each other if they have the same letters but in different orders.

This week’s challenge is a two-parter.

  1. Write a function to determine if two words are anagrams.
  2. Given a dictionary of words (such as this one), find all of the anagrams for a target word.

Bonus points for efficiency.

As usual, please send me your answers. I’ll share them all in next week’s issue. If you send me one, but you don’t want me to share it publicly, please let me know.

Rock on!
Eric Normand

The post PurelyFunctional.tv Newsletter 332: Tool: jEnv appeared first on PurelyFunctional.tv.

Permalink

What is a higher-order function?

Higher-order functions are used a lot in functional programming. They are functions that take other functions as arguments or return functions as return values, or both. You’ll probably be familiar with map, filter, and reduce, which are higher-order functions. In this episode, we go kind of deep on them.

Transcript

Eric Normand: What is a higher-order function? By the end of this episode, you should be able to identify them, understand why they’re useful, and have some higher-order functions to study and learn, if you don’t already know them.

My name is Eric Normand, and I help people thrive with functional programming. Higher-order functions play a big role in functional programming. You’ll hear the term a lot, and we’re going to learn about what the term is. They’re a powerful way to abstract out functionality.

Let’s get to it. First, the definition. A higher-order function is a function that either takes a function as an argument, returns a function as its return value, or both. We tend to use this all the time. If you’re a JavaScript programmer, if you’ve ever done any JavaScript, you’ve probably used them all over the place.

Many functions in JavaScript take a callback function as an argument. Any of those functions that take a callback function, those are higher-order functions. You might be more familiar with the classical three main functional higher-order functions, which are Map, Filter, and Reduce.

Map takes a function and an array or a list, and it returns an array of the F to function applied to all the elements of the argument. Filter takes a function, a predicate function, and a list of elements. It keeps all the ones that return true when you call the predicate on it.

Then, Reduce takes a function, an initial value, and an array, and runs the function on the initial value and each of the elements in the array, iteratively, and then returns the value at the end. These are all functions that take a function as an argument. We’ll call them first-order functions.

Where does the term come from? I don’t know if there’s a real origin, if this is the actual origin, but the way I like to think of it is there’s the notion of the order of a polynomial. A polynomial in math is some equation that’s like, 4x^2+3x+2. You could have, 7x^3+4x^2+3x+2.

Those are polynomials, but the order of the polynomial is the biggest exponent on the x. A third-order polynomial is going to have a three, a second order polynomial is going to have a two in the exponent. I like to look at it like that. That a zero-order function is a function that takes values, non-functions. It just takes values and returns values.

A first-order function is going to take a function. A second-order function is going to take a function that takes a function. It starts to get pretty abstract and hard to work with, but that first-order level is useful.

Why would we do this? Well, if you look at the examples of Map, Filter, and Reduce, they are basically specific use cases of for loops or recursion, perhaps, if that’s how you want to implement them, that are very useful and repeated a lot.

You could write the recursion yourself, or the for loop yourself, that transforms all the elements in a list and returns that new list with the new elements. You could do that, and every time you’d write the same for loop or the same recursion over and over again.

What Map does is it lets you not repeat that for loop and talk about what’s different, and that’s nice. It’s way less error-prone to have the thing be…The looping is done for you. All you have to do is provide this function, and the function turns out to be the body of the for loop.

Whatever you were doing inside that for loop now, you just put it in a function and you pass it in. Same with Filter. You could write the loop that goes through each element and checks it to see if it’s greater than 10. If it’s greater than 10, it adds it to a list. If it’s not greater than 10, it ignores it.

It loops through all the things. At the end, you’ll have a list of all the things in the original list that are greater than 10. What Filter lets you do is abstract that away.

You say, “I do not need to care about the for loop anymore, there’s a lot of errors that I could do in the for loop. Right now, I’m just going to pass the greater than 10 in as a function,” and Filter will take care of the rest. Reduce is similar. This lets us build new abstractions that help us reduce that repeated code.

Map, Filter, and Reduce are pretty universal. You’ll find them in a lot of languages, in the standard libraries. Because if you’ve got a general purpose language, these are general purpose tools, they work very well, like in any domain.

You could have some in your domain that wouldn’t make sense to be put in a general purpose languages, standard library. You can find them and you can write them yourselves. That’s why we had to learn higher-order functions.

If you’re using higher-order functions, you should be able to find these abstractions that can be turned into functions, and adding new features should get easier over time, because you’re increasing the number of abstractions that you have at your disposal.

Each time, you should be eliminating bugs, you should be making it easier to write, things like that.

We talked about functions that take functions as arguments. What about functions that return a function? We don’t see that so much in JavaScript. It’s not so common. We tend to want to write our functions ourselves in those languages. What would be an example of this?

Imagine a function, a very simple function, that takes an argument and then it returns a function that always returns that argument, no matter what arguments you pass it, it’s constantly. You pass a three, it will return a function that always returns three, so it’s constantly three.

Now, this function that always returns three is very easy to write. You can say function return three, and you’ve got that. As a little construct, it might be faster to say constantly three than to write out a whole function. Depends on how verbose your language is.

That’s an example of a function that takes an argument, just a number, or whatever value, and it returns a function that always returns that value. That’s not so useful, but sometimes it’s exactly what you need. It’s exactly what you need, but the [laughs] reason it’s not useful is because it’s so easy to write that function yourself. It makes a good example.

Imagine another function that takes a function and returns a function that returns the opposite of it. You pass in the function, “even,” so, “isEven,” which says, it returns true if the number is even and false if it’s not. You pass in this function, “isEven,” and it returns a function basically with “not” wrapped around it.

It’s a new function, but it’s calling “notIsEven,” so that’s odd. Now, of course, this is easy to write on your own. You can easily do that. When you build up a collection of these little combinators, is what they’re called, little functions that return or transform functions, return new functions, you can compose them.

You can do a thing that composes constantly with a not, with whatever you want, do it three times. There’s all sorts of things you could do to transform this thing so that you’re operating at this higher level. Instead of writing out the code, you’re doing higher-order transformations and stuff.

You don’t want to get too abstract. Sometimes it’s better to just write it out, but it’s possible. When you get to the next level of thinking, that’s where these things actually start to become very useful. I do want to say, I sometimes see this as related to what, in the object-oriented world, people call, dependency injection.

Dependency injection often, to me, looks like it’s an object’s constructor that takes objects as arguments. You’re passing in behavior. It’s like you’re passing in a function. It’s a similar idea to higher-order function that’s kind of higher-order objects. Objects made of other objects that get their behavior from those other objects.

Instead of referring to the objects in the global space or constructing them themselves, they allow them to be passed in.

Let me recap the definition of higher-order functions. These are functions that either takes a function as an argument, returns a function, or both. That’s possible, too. We use them all the time in JavaScript. So many functions have callbacks. Every time we’re passing a callback to another function, that function is higher-order.

You need first class functions for this to work. You need to be able to pass functions as arguments and return them as return values. Some common ones that you might know — Map, Filter, and Reduce. I call these the three functional tools. They’re your basic tool set for doing functional data transformation.

Higher-order functions help you build new abstractions. Notice, you don’t need to have a lot of the constructs of your language. Like the syntactic constructs can be rewritten in terms of higher-order functions. That’s something to try.

Try to write a conditional, like a function called, “If.” I know you probably can’t call it “If,” you can call it, “myIf,” that takes a Boolean and two functions, one for the then and one for the else. Give it a shot, see if you can do it.

You can also return a function. That is something that does come in handy, but we’re not going to go too deep into that. I think it’s related to dependency injection.

Do give that a shot. I’ll give you a little assignment. Try to write a conditional function that takes a Boolean and two other functions and decides which function to run based on that Boolean’s value.

All right. My name is Eric Normand. This has been my thoughts on functional programming. You can find this episode and all other episodes at lispcast.com/podcast.

You will find video recordings, audio, and text transcripts of everything there. You’ll also find links to subscribe and how to follow me on social media, how to get in touch with me. Awesome. I appreciate you being here, and rock on.

The post What is a higher-order function? appeared first on LispCast.

Permalink

Ep 034: Break the Mold

Christoph finds exceptional log lines and takes a more literal approach.

  • Previously, we upgraded our log parsing to handle finding two errors instead of just one.
  • “It’s amazing what you don’t find when you’re not looking.”
  • We ended up with a set of functions that can parse out multiple errors.
  • The result is a nice mixed sequence of errors that we can aggregate and pivot with Clojure core.
  • “By the power of Clojure core, I will map and reduce!”
  • (02:43) New Problem: there are exceptions in the log, and they span multiple lines!
  • The exception continuation lines don’t parse.
  • They don’t have a date, a log level, or anything else that regular log lines have.
  • How do we collect up all those lines? They are all part of one logical error.
  • Our error parsers don’t get a chance to see these lines, because the general line parser threw them away.
  • (06:27) Solution 1: Don’t pre-parse the lines, but instead have each error parsing function do the general parse.
  • Each parsing function would receive the raw, unparsed, line as a string.
  • Each function would have to run parse-line on the inputs before doing their specific parsing.
  • Now each function in our inventory needs to re-do the general parsing of the line.
  • This ends up being much more inefficient.
  • (10:13) Solution 2: Relax the general parsing.
  • What is the general parser to do with a line that doesn’t match the regexp?
  • “How about if the general parse does the best job it can, and whatever it can’t do, it doesn’t do?”
  • One of the keys in the data returned from parse-line is :raw/line, which is the entire line, and that’s always there.
  • So, if the regexp fails, we can always return at least that.
  • The map doesn’t have the keys that are not found.
  • Now, parse-line will always return a map for each line.
  • We’ve uplifted the data, at least slightly, from a string to a map.
  • We can know two things
    • Every map will have :raw/line
    • If a map doesn’t have the other keys (like :log/date), that means it didn’t parse, and is probably a continuation of a previous line.
  • This is us expanding our program’s view of reality to more closely match actual reality.
  • We left out part of reality, and got stuck because of it.
  • Being more literal to the data source gives us more flexibility.
  • Clojure’s dynamic nature shines in this situation. We don’t have to have all the keys all the time.
  • “A map is a bucket of dimensions.”
  • Each function that operates on the data will inspect the data and see if it has all the keys it needs. It doesn’t matter if there are other keys.
  • Namespaced keys really help in this situation, each new level of parsing can add keys without overwriting keys from other dimensions.
  • How does this impact our parsing functions?
  • They are unchanged, because they grab the message out using some->>, which shortcuts on nil.
  • (19:08) Our exception error parsing function detect the start in a number of ways.
    • The next line is a bare line?
    • The first line ends in an opening curly brace?
  • Then, to find the end of the exception, it can use take-while to find all the lines that are bare.
  • To assist in finding bare lines, we can introduce a predicate function instead of embedding that logic.
  • When all lines are found, it then combines them into a new :log/message key.
  • When updating the error map in our parsing functions, we are careful to only operate on the data we know is there, so that other keys are not impacted.

Related episodes:

Clojure in this episode:

  • some->>, take-while

Code sample from this episode:

(ns devops.week-06
  (:require
    [clojure.string :as string]
    [devops.week-02 :refer [process-log]]
    [devops.week-05 :refer [parse-357-error parse-sprinkle]]
    ))


(def general-re #"(\d\d\d\d-\d\d-\d\d)\s+(\d\d:\d\d:\d\d)\s+\|\s+(\S+)\s+\|\s+(\S+)\s+\|\s+(\S+)\s+\|\s(.*)")

(defn parse-line
  [line]
  (if-let [[whole dt tm thread-name level ns message] (re-matches general-re line)]
    {:raw/line whole
     :log/date dt
     :log/time tm
     :log/thread thread-name
     :log/level level
     :log/namespace ns
     :log/message message}
    {:raw/line line
     :log/message line}))

(defn bare-line?
  [line]
  (nil? (:log/date line)))

(defn parse-exception-info
  [lines]
  (let [first-line (first lines)
        [_whole classname] (some->> first-line :log/message (re-matches #"([^ ]+) #error \{"))]
    (when classname
      (let [error-lines (cons first-line (take-while bare-line? (rest lines)))
            error-str (string/join "\n" (map :log/message error-lines))]
        (merge first-line
               {:kind :error
                :error/class classname
                :log/message error-str})))))

(defn parse-next
  [lines]
  (or (parse-357-error lines)
      (parse-sprinkle lines)
      (parse-exception-info lines)))

(defn parse-all
  [lines]
  (lazy-seq
    (when (seq lines)
      (if-some [found (parse-next lines)]
        (cons found (parse-all (rest lines)))
        (parse-all (rest lines))))))


(comment
  (process-log "sample.log" #(->> % (map parse-line) parse-all doall))
  )

Log file sample:

2019-05-14 16:48:57 | process-Poster | INFO  | com.donutgram.poster | transaction failed while updating user joe: code 357
2019-05-14 16:48:55 | process-Poster | INFO  | com.donutgram.poster | failed to add sprinkle to donut 50493
2019-05-14 16:48:55 | process-Poster | INFO  | com.donutgram.poster | sprinkle fail reason: unknown state
2019-05-14 16:48:55 | process-Poster | INFO  | com.donutgram.poster | Poster #error {
 :cause "Failed to lock the synchronizer"
 :data {}
 :via
 [{:type clojure.lang.ExceptionInfo
   :message "Failed to lock the synchronizer"
   :data {}
   :at [process.poster$eval50560 invokeStatic "poster.clj" 40]}]
 :trace
 [[process.poster$eval50560 invokeStatic "poster.clj" 40]
  [clojure.lang.AFn run "AFn.java" 22]
  [java.lang.Thread run "Thread.java" 748]]}

Permalink

nREPL 0.7: Introducing a Native EDN Transport

Today marks the release of the first milestone from the nREPL 0.7 series - namely nREPL 0.7.0-alpha1. The highlight of the release is the addition of a native (built-in) EDN transport to complement the default bencode transport that nREPL has shipped since day 1.

Understanding the EDN Transport

The main difference between the bencode transport and the EDN one is that instead of dealing with bencode dictionaries and lists, and being limited to integer and byte strings, you’d be sending and receiving EDN data, including maps, vectors, lists, sets, strings, keywords and symbols. The structure of the messages is pretty much the same, only the data format changes. This may be useful in a couple of usecases:

  • In some clients, including ClojureScript ones, where EDN is easier to handle than bencode (as it’s supported out of the box).
  • This transport exposes richer data types and potentially we can have middleware return different data when using it.

You should keep in mind, however, that it’s not clear whether it’s a good idea to return different data based on the data format, so for now we’ve opted to return exactly the same data in bencode and EDN responses. Currently we’re mostly aiming to address the first point - making it easier for some clients to speak with nREPL. In this sense the project is pretty similar to Fastlane which brought JSON and MessagePack transports to nREPL. We remain committed to bencode and we believe that it’s the optimal default encoding format due to its simplicity and universal nature, but we also acknowledge that EDN might be more convenient in a Java or JavaScript environment.

The introduction of the EDN transport does require more specification of the message format, as bencode’s simplicity left some room for interpretation here and there (e.g. for bencode strings, symbols and keywords are basically the same thing). Some of the things that we had to specify for the EDN requests are that :op values are strings, and :status is either a keyword, or a set of keywords. This may cause some surprising, unintended consequences, thus tweaks to the EDN message format may be possible until the transport is declared mature. Documentation of the message format using clojure.spec is planned to help address any confusion. Now let’s take a close look at a practical examples of those differences. Consider the following four eval requests:

{:op "eval"  ...}
{:op :eval ...}
{"op" "eval" ...}
{"op" :eval ...}

The first one is considered canonical, though nREPL will accept the second one as well. The third and fourth one will not work, as ops have to be keywords if you’re using the EDN transport. Note that the change doesn’t affect messages encoded with bencode, but mostly direct usage of the nREPL client API. That’s why we assume that the impact from it would be small if any.

Using the EDN Transport

I guess by now you’re wondering how can you play with the new transport yourselves. Using the EDN transport is pretty simple. You just need to start an nREPL server with EDN transport and you’re good to go:

(require
 '[nrepl.server :as server]
 '[nrepl.transport :as transport])

(server/start-server :port 12345 :transport-fn transport/edn)

You can also start an nREPL with a EDN transport using clj1:

$ clj -Sdeps '{:deps {nrepl {:mvn/version "0.7.0-alpha1"}}}' -m nrepl.cmdline -t nrepl.transport/edn
nREPL server started on port 63266 on host localhost - nrepl+edn://localhost:63266

Right now there are no 3rd party clients that support the EDN transport, but you can also use nREPL built-in client to test it:

$ clj -Sdeps '{:deps {nrepl {:mvn/version "0.7.0-alpha1"}}}' -m nrepl.cmdline -t nrepl.transport/edn --connect --host localhost --port 63266

Alternatively you can just play with the client in the REPL:

(require '[nrepl.core :as nrepl])

(with-open [conn (nrepl/url-connect "nrepl+edn://localhost:63266")]
  (-> (nrepl/client conn 1000)    ; message receive timeout required
      (nrepl/message {:op "eval" :code "(+ 1 2 3)"})
      nrepl/response-values))

;; => 6

As you can see nrepl/url-connect is smart enough to figure out which transport to use based on the URI scheme (in this case “nrepl+edn”).

There’s nothing magical about the EDN transport and using it is pretty much transparent at this point.

Closing Thoughts

That’s the first iteration of the EDN transport and it’s probably rough around the edge here and there. Getting some testing and feedback would be awesome! I hope this post will inspire at least a few of you to play with the new transport and find some good use for it!

I’ll also use this post as an opportunity to invite all of you to check out the open tickets on nREPL’s issue tracker and try your hand at tackling some of them. It’s lots of fun!

  1. You can also specify the nREPL transport to use with Leiningen or via nrepl.edn. I chose clj for this example for simplicity’s sake. 

Permalink

Sudoku in Reagent

Here's the live code - the game is rendered below.

We'll start with a bunch of sudokus:

(require '[reagent.core :refer [atom]])

(def sudokus
  [{:puzzle [0 0 4 3 0 0 2 0 9 0 0 5 0 0 9 0 0 1 0 7 0 0 6 0 0 4 3 0 0 6 0 0 2 0 8 7 1 9 0 0 0 7 4 0 0 0 5 0 0 8 3 0 0 0 6 0 0 0 0 0 1 0 5 0 0 3 5 0 8 6 9 0 0 4 2 9 1 0 3 0 0]
    :answer [8 6 4 3 7 1 2 5 9 3 2 5 8 4 9 7 6 1 9 7 1 2 6 5 8 4 3 4 3 6 1 9 2 5 8 7 1 9 8 6 5 7 4 3 2 2 5 7 4 8 3 9 1 6 6 8 9 7 3 4 1 2 5 7 1 3 5 2 8 6 9 4 5 4 2 9 1 6 3 7 8]}
   {:puzzle [0 4 0 1 0 0 0 5 0 1 0 7 0 0 3 9 6 0 5 2 0 0 0 8 0 0 0 0 0 0 0 0 0 0 1 7 0 0 0 9 0 6 8 0 0 8 0 3 0 5 0 6 2 0 0 9 0 0 6 0 5 4 3 6 0 0 0 8 0 7 0 0 2 5 0 0 9 7 1 0 0]
    :answer [3 4 6 1 7 9 2 5 8 1 8 7 5 2 3 9 6 4 5 2 9 6 4 8 3 7 1 9 6 5 8 3 2 4 1 7 4 7 2 9 1 6 8 3 5 8 1 3 7 5 4 6 2 9 7 9 8 2 6 1 5 4 3 6 3 1 4 8 5 7 9 2 2 5 4 3 9 7 1 8 6]}
   {:puzzle [6 0 0 1 2 0 3 8 4 0 0 8 4 5 9 0 7 2 0 0 0 0 0 6 0 0 5 0 0 0 2 6 4 0 3 0 0 7 0 0 8 0 0 0 6 9 4 0 0 0 3 0 0 0 3 1 0 0 0 0 0 5 0 0 8 9 7 0 0 0 0 0 5 0 2 0 0 0 1 9 0]
    :answer [6 9 5 1 2 7 3 8 4 1 3 8 4 5 9 6 7 2 7 2 4 8 3 6 9 1 5 8 5 1 2 6 4 7 3 9 2 7 3 9 8 1 5 4 6 9 4 6 5 7 3 8 2 1 3 1 7 6 9 2 4 5 8 4 8 9 7 1 5 2 6 3 5 6 2 3 4 8 1 9 7]}
   {:puzzle [4 9 7 2 0 0 0 0 0 1 0 0 4 0 0 0 0 5 0 0 0 0 1 6 0 9 8 6 2 0 3 0 0 0 4 0 3 0 0 9 0 0 0 0 0 0 0 1 0 7 2 6 0 0 0 0 2 0 0 5 8 7 0 0 0 0 6 0 0 0 0 4 5 3 0 0 9 7 0 6 1]
    :answer [4 9 7 2 5 8 3 1 6 1 8 6 4 3 9 7 2 5 2 5 3 7 1 6 4 9 8 6 2 9 3 8 1 5 4 7 3 7 5 9 6 4 1 8 2 8 4 1 5 7 2 6 3 9 9 6 2 1 4 5 8 7 3 7 1 8 6 2 3 9 5 4 5 3 4 8 9 7 2 6 1]}
   {:puzzle [0 0 5 9 1 0 3 0 8 0 0 9 4 0 3 0 6 0 0 2 7 5 0 0 1 0 0 0 3 0 0 0 0 2 0 1 0 0 0 8 2 0 0 0 7 0 0 6 0 0 7 0 0 4 0 0 0 0 8 0 0 0 0 6 4 0 1 5 0 7 0 0 8 9 0 0 0 0 4 2 0]
    :answer [4 6 5 9 1 2 3 7 8 1 8 9 4 7 3 5 6 2 3 2 7 5 6 8 1 4 9 7 3 8 6 4 5 2 9 1 9 5 4 8 2 1 6 3 7 2 1 6 3 9 7 8 5 4 5 7 3 2 8 4 9 1 6 6 4 2 1 5 9 7 8 3 8 9 1 7 3 6 4 2 5]}
   {:puzzle [1 0 0 0 0 5 0 0 7 3 8 0 9 0 0 0 0 0 6 0 0 0 0 0 4 8 0 8 2 0 0 0 1 0 7 5 0 4 0 7 6 0 0 2 0 0 6 9 0 0 2 0 0 1 0 0 5 0 3 9 0 0 4 0 0 0 0 2 0 1 0 0 0 0 0 0 4 6 3 5 2]
    :answer [1 9 4 6 8 5 2 3 7 3 8 2 9 7 4 5 1 6 6 5 7 2 1 3 4 8 9 8 2 3 4 9 1 6 7 5 5 4 1 7 6 8 9 2 3 7 6 9 3 5 2 8 4 1 2 1 5 8 3 9 7 6 4 4 3 6 5 2 7 1 9 8 9 7 8 1 4 6 3 5 2]}
   {:puzzle [0 0 9 0 6 5 4 3 0 0 0 7 0 0 0 8 0 0 6 0 0 1 0 8 0 2 0 0 0 3 0 9 0 0 0 2 5 0 1 4 0 3 9 6 0 8 0 4 0 0 0 1 0 0 0 3 0 5 0 9 0 0 7 0 5 6 0 8 0 0 0 0 0 7 0 2 4 0 0 9 0]
    :answer [2 8 9 7 6 5 4 3 1 3 1 7 9 2 4 8 5 6 6 4 5 1 3 8 7 2 9 7 6 3 8 9 1 5 4 2 5 2 1 4 7 3 9 6 8 8 9 4 6 5 2 1 7 3 4 3 2 5 1 9 6 8 7 9 5 6 3 8 7 2 1 4 1 7 8 2 4 6 3 9 5]}
   {:puzzle [0 0 0 0 0 0 6 5 7 7 0 2 4 0 0 1 0 0 3 5 0 0 0 6 0 0 0 5 0 0 0 2 0 0 0 9 2 1 0 3 0 0 5 0 0 0 4 7 1 0 9 0 0 8 0 0 8 7 6 0 0 9 0 9 0 0 5 0 2 0 3 0 0 3 0 0 1 8 2 0 6]
    :answer [8 9 4 2 3 1 6 5 7 7 6 2 4 9 5 1 8 3 3 5 1 8 7 6 9 4 2 5 8 3 6 2 4 7 1 9 2 1 9 3 8 7 5 6 4 6 4 7 1 5 9 3 2 8 1 2 8 7 6 3 4 9 5 9 7 6 5 4 2 8 3 1 4 3 5 9 1 8 2 7 6]}
   {:puzzle [5 0 3 0 7 0 1 9 0 0 0 0 0 0 6 7 5 0 0 4 7 1 9 0 6 0 0 4 0 0 0 3 8 0 0 0 9 5 0 2 0 0 3 0 0 0 0 0 0 1 0 0 7 2 0 0 0 8 0 4 0 0 1 3 0 0 0 0 1 8 6 0 0 8 6 7 2 0 0 0 5]
    :answer [5 6 3 4 7 2 1 9 8 2 1 9 3 8 6 7 5 4 8 4 7 1 9 5 6 2 3 4 7 2 6 3 8 5 1 9 9 5 1 2 4 7 3 8 6 6 3 8 5 1 9 4 7 2 7 9 5 8 6 4 2 3 1 3 2 4 9 5 1 8 6 7 1 8 6 7 2 3 9 4 5]}
   {:puzzle [0 6 0 7 2 0 9 0 8 0 8 4 0 0 3 0 0 1 7 0 0 1 0 0 0 6 5 9 0 0 0 0 8 0 0 0 0 7 1 0 6 0 0 0 0 0 0 2 0 1 0 0 3 4 0 0 0 2 0 0 7 0 6 0 3 0 0 4 9 8 0 0 2 1 5 0 0 0 0 9 0]
    :answer [1 6 3 7 2 5 9 4 8 5 8 4 6 9 3 2 7 1 7 2 9 1 8 4 3 6 5 9 4 6 3 5 8 1 2 7 3 7 1 4 6 2 5 8 9 8 5 2 9 1 7 6 3 4 4 9 8 2 3 1 7 5 6 6 3 7 5 4 9 8 1 2 2 1 5 8 7 6 4 9 3]}
   {:puzzle [1 4 0 0 6 0 8 0 0 0 8 5 0 1 0 0 4 0 9 0 7 4 0 0 2 5 0 0 3 0 0 7 0 4 0 0 2 0 9 0 0 0 3 0 7 0 0 8 9 0 0 0 6 0 0 0 0 7 4 0 0 1 0 6 0 1 3 0 5 0 9 0 7 0 0 0 0 2 6 0 0]
    :answer [1 4 2 5 6 9 8 7 3 3 8 5 2 1 7 9 4 6 9 6 7 4 3 8 2 5 1 5 3 6 8 7 1 4 2 9 2 1 9 6 5 4 3 8 7 4 7 8 9 2 3 1 6 5 8 9 3 7 4 6 5 1 2 6 2 1 3 8 5 7 9 4 7 5 4 1 9 2 6 3 8]}
   {:puzzle [5 9 0 0 0 0 1 4 7 0 0 0 9 0 0 0 0 8 0 7 2 0 0 0 0 3 0 7 0 0 0 4 0 2 9 0 0 2 0 0 3 0 8 0 6 8 0 0 1 7 0 0 5 0 0 0 5 7 6 4 0 0 9 0 3 6 0 0 5 0 0 0 1 0 0 8 0 0 0 0 2]
    :answer [5 9 8 3 2 6 1 4 7 3 1 4 9 5 7 6 2 8 6 7 2 4 8 1 9 3 5 7 5 3 6 4 8 2 9 1 4 2 1 5 3 9 8 7 6 8 6 9 1 7 2 4 5 3 2 8 5 7 6 4 3 1 9 9 3 6 2 1 5 7 8 4 1 4 7 8 9 3 5 6 2]}
   {:puzzle [1 0 0 0 0 0 0 9 0 2 0 8 9 7 0 6 0 5 0 0 0 5 3 2 0 0 0 0 0 6 0 5 0 4 0 0 7 0 0 8 0 6 0 0 2 0 8 3 7 0 0 0 1 0 6 0 4 0 8 0 1 2 0 8 9 0 6 0 0 0 5 0 0 1 5 0 4 0 0 0 7]
    :answer [1 5 7 4 6 8 2 9 3 2 3 8 9 7 1 6 4 5 4 6 9 5 3 2 7 8 1 9 2 6 1 5 3 4 7 8 7 4 1 8 9 6 5 3 2 5 8 3 7 2 4 9 1 6 6 7 4 3 8 5 1 2 9 8 9 2 6 1 7 3 5 4 3 1 5 2 4 9 8 6 7]}
   {:puzzle [9 0 0 0 8 4 0 6 0 6 0 4 0 0 5 2 0 7 0 3 0 0 7 0 0 8 0 7 6 0 0 0 1 5 0 0 0 5 3 0 0 0 0 0 1 0 0 0 4 0 9 6 0 3 1 0 5 0 2 6 0 9 0 0 0 2 0 4 0 0 0 0 8 0 0 0 0 3 7 1 0]
    :answer [9 2 7 3 8 4 1 6 5 6 8 4 9 1 5 2 3 7 5 3 1 6 7 2 4 8 9 7 6 9 2 3 1 5 4 8 4 5 3 7 6 8 9 2 1 2 1 8 4 5 9 6 7 3 1 7 5 8 2 6 3 9 4 3 9 2 1 4 7 8 5 6 8 4 6 5 9 3 7 1 2]}
   {:puzzle [3 0 8 0 5 6 0 0 7 0 0 6 9 0 0 2 5 3 0 1 2 0 4 0 0 0 0 0 0 0 0 0 0 3 2 0 9 0 4 8 0 0 0 0 0 7 6 0 1 0 9 8 0 5 0 0 0 0 0 1 9 0 4 8 3 1 0 0 0 5 0 6 0 4 0 0 0 7 0 3 0]
    :answer [3 9 8 2 5 6 4 1 7 4 7 6 9 1 8 2 5 3 5 1 2 7 4 3 6 9 8 1 8 5 6 7 4 3 2 9 9 2 4 8 3 5 7 6 1 7 6 3 1 2 9 8 4 5 2 5 7 3 6 1 9 8 4 8 3 1 4 9 2 5 7 6 6 4 9 5 8 7 1 3 2]}
   {:puzzle [1 7 0 3 0 0 0 0 9 0 0 8 0 4 0 6 0 0 0 0 0 0 6 0 0 3 0 6 0 0 8 0 0 0 0 1 9 2 4 6 0 0 3 0 0 3 0 0 9 0 2 5 0 0 0 1 0 2 0 0 0 4 0 7 0 9 5 0 3 0 1 6 0 0 5 0 0 7 8 0 0]
    :answer [1 7 6 3 2 8 4 5 9 5 3 8 1 4 9 6 7 2 4 9 2 7 6 5 1 3 8 6 5 7 8 3 4 9 2 1 9 2 4 6 5 1 3 8 7 3 8 1 9 7 2 5 6 4 8 1 3 2 9 6 7 4 5 7 4 9 5 8 3 2 1 6 2 6 5 4 1 7 8 9 3]}
   {:puzzle [0 0 4 0 3 0 0 2 1 0 7 0 0 0 5 0 0 9 3 8 0 6 9 0 0 0 0 0 3 0 0 0 0 0 0 0 6 0 2 1 0 0 4 5 0 0 1 0 9 0 7 0 0 3 0 0 0 8 4 6 7 0 0 5 6 0 0 0 1 2 4 0 0 0 8 2 5 0 0 3 0]
    :answer [9 5 4 7 3 8 6 2 1 2 7 6 4 1 5 3 8 9 3 8 1 6 9 2 5 7 4 8 3 7 5 6 4 9 1 2 6 9 2 1 8 3 4 5 7 4 1 5 9 2 7 8 6 3 1 2 3 8 4 6 7 9 5 5 6 9 3 7 1 2 4 8 7 4 8 2 5 9 1 3 6]}
   {:puzzle [0 8 3 2 0 0 0 9 6 2 0 0 0 3 0 7 0 4 0 0 7 9 1 5 0 0 0 4 0 2 3 9 0 0 0 8 0 1 0 0 0 4 0 6 0 0 6 9 8 7 0 0 0 0 0 0 0 4 0 0 0 0 7 5 0 0 0 6 0 2 8 0 0 7 0 0 5 0 9 0 0]
    :answer [1 8 3 2 4 7 5 9 6 2 9 5 6 3 8 7 1 4 6 4 7 9 1 5 8 3 2 4 5 2 3 9 6 1 7 8 7 1 8 5 2 4 3 6 9 3 6 9 8 7 1 4 2 5 9 2 1 4 8 3 6 5 7 5 3 4 7 6 9 2 8 1 8 7 6 1 5 2 9 4 3]}
   {:puzzle [8 0 3 0 0 0 2 7 0 4 0 9 0 0 8 0 0 0 7 0 0 0 2 4 0 9 6 0 0 0 0 0 6 9 1 5 0 0 1 8 0 2 0 0 0 0 3 0 7 5 0 0 0 0 0 5 4 0 0 0 0 6 0 6 0 8 1 0 0 0 0 3 3 7 2 0 0 9 1 4 0]
    :answer [8 6 3 9 1 5 2 7 4 4 2 9 6 7 8 3 5 1 7 1 5 3 2 4 8 9 6 2 8 7 4 3 6 9 1 5 5 4 1 8 9 2 6 3 7 9 3 6 7 5 1 4 8 2 1 5 4 2 8 3 7 6 9 6 9 8 1 4 7 5 2 3 3 7 2 5 6 9 1 4 8]}
   {:puzzle [0 7 0 4 9 0 1 0 3 0 0 3 0 7 0 5 9 0 0 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 6 1 1 0 0 7 4 9 0 2 0 0 2 4 3 0 6 0 0 8 6 0 0 9 8 0 7 0 0 0 1 2 6 0 0 0 0 0 4 8 0 0 0 7 0 5 2]
    :answer [2 7 6 4 9 5 1 8 3 8 4 3 2 7 1 5 9 6 9 5 1 8 6 3 2 4 7 3 9 7 5 2 8 4 6 1 1 6 8 7 4 9 3 2 5 5 2 4 3 1 6 9 7 8 6 3 5 9 8 2 7 1 4 7 1 2 6 5 4 8 3 9 4 8 9 1 3 7 6 5 2]}
   {:puzzle [8 3 0 0 4 0 0 9 6 0 2 0 0 1 0 0 0 8 9 0 4 7 0 0 0 3 0 4 0 9 0 0 2 0 6 5 3 0 8 0 0 1 0 7 0 0 0 0 6 0 3 8 0 0 5 0 7 0 3 0 0 2 0 0 0 0 5 0 6 4 0 0 0 0 2 0 8 0 1 0 0]
    :answer [8 3 1 2 4 5 7 9 6 7 2 6 3 1 9 5 4 8 9 5 4 7 6 8 2 3 1 4 1 9 8 7 2 3 6 5 3 6 8 4 5 1 9 7 2 2 7 5 6 9 3 8 1 4 5 8 7 1 3 4 6 2 9 1 9 3 5 2 6 4 8 7 6 4 2 9 8 7 1 5 3]}
   {:puzzle [0 6 0 2 5 0 0 0 0 7 9 2 0 0 6 1 0 0 0 0 0 0 8 1 6 0 0 0 0 9 0 0 0 5 0 0 4 1 0 0 0 9 7 8 0 2 0 7 3 0 0 0 0 4 0 0 0 7 6 3 0 1 0 3 0 0 5 4 0 2 9 0 8 0 0 0 0 0 0 4 0]
    :answer [1 6 8 2 5 7 4 3 9 7 9 2 4 3 6 1 5 8 5 4 3 9 8 1 6 7 2 6 3 9 8 7 4 5 2 1 4 1 5 6 2 9 7 8 3 2 8 7 3 1 5 9 6 4 9 2 4 7 6 3 8 1 5 3 7 1 5 4 8 2 9 6 8 5 6 1 9 2 3 4 7]}
   {:puzzle [0 5 0 4 0 0 6 8 0 0 9 0 1 0 0 0 0 0 0 0 8 0 5 9 3 0 2 0 0 7 2 0 3 0 0 0 0 0 0 6 0 0 2 0 8 6 0 4 0 8 0 0 0 5 0 3 6 0 0 4 1 9 0 1 0 0 0 0 7 0 0 0 0 7 2 8 0 0 0 5 0]
    :answer [7 5 1 4 3 2 6 8 9 2 9 3 1 6 8 5 7 4 4 6 8 7 5 9 3 1 2 5 8 7 2 4 3 9 6 1 3 1 9 6 7 5 2 4 8 6 2 4 9 8 1 7 3 5 8 3 6 5 2 4 1 9 7 1 4 5 3 9 7 8 2 6 9 7 2 8 1 6 4 5 3]}
   {:puzzle [0 6 1 4 9 0 0 2 0 2 8 0 0 0 7 0 5 0 0 0 3 1 0 8 0 0 7 6 0 0 7 0 4 0 3 1 0 0 0 2 5 0 0 7 4 0 9 0 6 0 0 0 0 0 0 0 0 0 1 0 0 0 8 5 7 0 0 0 0 2 0 6 8 0 0 9 0 6 0 0 0]
    :answer [7 6 1 4 9 5 8 2 3 2 8 4 3 6 7 1 5 9 9 5 3 1 2 8 4 6 7 6 2 5 7 8 4 9 3 1 1 3 8 2 5 9 6 7 4 4 9 7 6 3 1 5 8 2 3 4 6 5 1 2 7 9 8 5 7 9 8 4 3 2 1 6 8 1 2 9 7 6 3 4 5]}
   {:puzzle [6 0 8 9 0 0 0 5 0 0 0 0 3 2 0 1 9 0 0 1 0 0 0 0 3 0 0 4 0 0 0 7 3 6 0 0 5 7 0 2 6 0 0 0 0 0 0 3 1 0 5 0 2 0 0 8 0 0 0 0 0 6 4 0 2 0 0 9 0 5 0 7 0 4 7 0 0 8 0 0 1]
    :answer [6 3 8 9 1 7 4 5 2 7 5 4 3 2 6 1 9 8 2 1 9 5 8 4 3 7 6 4 9 2 8 7 3 6 1 5 5 7 1 2 6 9 8 4 3 8 6 3 1 4 5 7 2 9 1 8 5 7 3 2 9 6 4 3 2 6 4 9 1 5 8 7 9 4 7 6 5 8 2 3 1]}])

Now we'll shuffle them and pick one to be the current-puzzle.

(def current-puzzle (atom (first (shuffle sudokus))))

Assign a set of coordinates and a puzzle digit to each square and call it a board:

(defn positions []
    (for [i (range 9) j (range 9)]
      [j i]))
  
  (defn init-matrix []
    (into {}
          (map vector
               (positions)
               (:puzzle @current-puzzle))))
  
  (def board (atom (init-matrix)))

A cell, when clicked becomes the selected-cell, and is displayed in a different color.

(def selected-cell (atom nil))

(def mouse-over-cell (atom nil))

(defn rect-cell [[x y]]
  [:rect.cell
   {:x (+ 0 x) :width 1
    :y (+ 0 y) :height 1
    :fill (cond
            (= [x y] @selected-cell) "magenta"
            (= [x y] @mouse-over-cell) "violet"
            (and (< 2 x 6) (< 2 y 6)) "lightpink"
            (or (< 2 x 6) (< 2 y 6)) "cyan"
            :else "lightpink")
    :stroke-width (if (or (= [x y] @selected-cell)
                          (= [x y] @mouse-over-cell))
                    0.05 0.025)
    :stroke "black"
    :on-click
    (fn click-square [e]
      (reset! selected-cell [x y]))
    :on-mouse-over
    (fn mouse-over-square [e]
      (reset! mouse-over-cell [x y]))}])
  
  (defn text-cell [app-state [x y]]
    [:text
     {:x (+ 0.5 x) :width 1
      :y (+ 0.72 y) :height 1
      :text-anchor "middle"
      :font-size 0.6
      :on-click
      (fn click-square [e]
        (reset! selected-cell [x y]))}
     (if (< 0 (get app-state [x y]))
       (get app-state [x y]))])
  
  (defn render-board [app-state]
    (into
     [:svg.board
      {:view-box "0 0 9 9"
       :shape-rendering "auto"
       :style {:max-height "500px"}}]
     (for [[pos val] app-state]
       [:g
        [rect-cell pos]
        [text-cell app-state pos]])))
  

The number-button will set the selected-cell in the board to its value.

(defn number-button [x]
  (fn []
     [:button
    {:style {:color "white"
             :padding "20 20px"
             :background-color "violet"
             :background-image "linear-gradient(to top left,
             rgba(0, 0, 0, .2),
             rgba(0, 0, 0, .2) 30%,
             rgba(0, 0, 0, 0))"
             :font-size "28px"
             :text-shadow "1px 1px 1px #000"
             :border-radius "10px"
             :box-shadow "inset 2px 2px 3px rgba(255, 255, 255, .6),
             inset -2px -2px 3px rgba(0, 0, 0, .6)"}
     :on-click
     (fn new-game-click [e]
       (swap! board assoc @selected-cell x))}
      x]))

To figure out if we've won, we compare the current digits with the answer:

(defn attempt [state]
  (into [] (for [i (range 9) j (range 9)]
             (get state [j i]))))

(defn win? [state]
  (= (:answer @current-puzzle)
     (attempt state)))

(defn num-row []
  (for [i (range 1 10)]
    [number-button i]))

(defn sudoku []
  [:center
   [:h2 "Sudoku - Reagent"]
   [:div (num-row)]
   [:div [render-board @board]]
   [:h3 (str "Win? = " (win? @board))]
   [:button
    {:style {:color "white"
             :padding "50 50px"
             :background-color "violet"
             :background-image "linear-gradient(to top left,
             rgba(0, 0, 0, .2),
             rgba(0, 0, 0, .2) 30%,
             rgba(0, 0, 0, 0))"
             :font-size "28px"
             :text-shadow "1px 1px 1px #000"
             :border-radius "10px"
             :box-shadow "inset 2px 2px 3px rgba(255, 255, 255, .6),
             inset -2px -2px 3px rgba(0, 0, 0, .6)"}
     :on-click
     (fn new-game-click [e]
       (reset! current-puzzle (first (shuffle sudokus)))
       (reset! board (init-matrix)))}
    "New puzzle"]])
[sudoku]

Permalink

The 3 levels of functional thinking

I’ve noticed that people go through a certain journey when learning functional programming. I’ve classified it into three levels: 1) Distinction between Actions, Calculations, and Data; and learning to use them effectively 2) Higher-order thinking; and building abstractions from higher-order functions 3) Algebraic thiking; building coherent models with a focus on composition. This is a work in progress and I’d love your input.

Transcript

Eric Normand: What are the three levels of functional thinking?

In this episode, I’m going to talk about my thinking about progress through the skills and thought processes that go into functional programming. My name is Eric Normand, and I help people thrive with functional programming.

I want to say that this is a work-in-progress. It is one way of mapping out the skills and categorizing them as a progression of skills. It’s not the only way, and I have no hard evidence about the skills being done in this order.

They are mostly anecdotal. I’m noticing that people might learn a bunch of stuff and then they get stuck or they’re in a certain spot, and they’re still progressing. They haven’t learned this other thing yet. It’s just me putting it together.

I’m not creating some model that people are going to have to stick to or anything. It’s mostly a way to organize the material that I’m putting into my book.

Here are the three levels. Remember, work-in progress. I’d love to discuss it, but I’m not going to die on this sword or anything.

The first one is the awareness and use of the distinction of actions, calculations, and data. Actions are things that depend on time. They depend on when they’re run and how many times they’re run. They have effects on the world or are affected by the world.

Calculations are computations from inputs to outputs. They don’t depend on time. If you give them the same inputs, they’re going to give you the same output. Finally, data is facts about events. It’s very inert. It doesn’t do anything on its own or requires interpretation.

When you’re in this first level, your main challenges are learning, with actions, how to deal with the time, how to manipulate time, to master it, so that you can guarantee the ordering of the actions when you need it guaranteed.

You can guarantee the things aren’t running at the same time if they shouldn’t be running at the same time, and guarantee that they happen the correct number of times. These are all the challenges that you face when you’re dealing with actions.

Calculations, the challenge here is to start modeling your program in terms of things. It can be very difficult for people who are coming from another paradigm to not use mutable state, to model things more as data transformations as opposed to step-by-step instructions like in an algorithm.

You’re learning to think about all the stuff that your program does that isn’t really necessary to be done as a side effect, as an action. There are some side effects that are necessary. You want your program to send an email, that it’s incorrect if it doesn’t send an email. That isn’t a necessary action.

Do you really need to use that global variable as scratch space for your algorithm? Probably not. If you don’t use it, none of your users are going to be upset. It’s still a correct program. That’s an unnecessary action.

We, as functional programmers, tend to frown upon unnecessary actions, and we want to convert them into calculations. That’s the challenge, learning how to do that. Sometimes it is relearning how to program even the simplest things using calculations instead of actions.

With the data, it’s about modeling. It’s about making sure that your data has the right structure to be able to support the algorithms that you need to do. It’s capturing the data you need. All that stuff comes under data modeling. Those are the three things you’re distinguishing as a functional programmer at level one. You’re learning to work with that.

You keep learning and you eventually get to level two, which is where you have higher-order thinking. You’ve mastered doing stuff with immutable data and thinking of things as data transformation, and you start to realize that there’s a lot of duplicated functionality.

You’ve been using, let’s say, for loops to make lists of things from other lists. You think, “Well, I could be doing this with a function that I pass the function to.” I’m essentially going to pass the body of the for loop as a function into Map. This is higher-order thinking. You start thinking in terms of pieces of algorithms that can be passed to other algorithms.

That’s another challenge that you come to. Some of the challenges, you could think of like dragons that you have to avoid. It’s over abstracting. It’s very easy to get carried away and have unreadable code because you’re using second or third order functions. It’s very difficult to see what’s going on.

Then there’s not using them enough and running the risk of not having a scalable piece of software, like your code does not scale. You have to write the same number of lines for every feature. A feature takes a 100 new lines of code. You need 20 features. That means you’re going to need 2,000 lines of code.

Whereas, if you’re using higher-order thinking, it should be easier to write the features each time, fewer lines of code because you’re able to find essential abstractions that work in your domain.

There are some that are universal. Map, Filter, and Reduce are very common. You should be able to find some that work only in your domain that don’t make sense to go in the standard library. This is number two — higher-order thinking. That’s where people start thinking in terms of functional programming as just like data transformation pipelines.

I’m doing Maps and Filters, and Maps and Filters, and I have these pipelines where all this work is getting done through the sequence functions. There’s another level. I’ve met a lot of people who get comfortable at level two and that’s where they stay.

There’s not a lot written about level three. The stuff that I’ve seen that’s written about it is often very abstract and obtuse. It’s abstract because it’s at next level so it’s going to seem out there. You can get there. I hope to help people get there. I hope to find a path that’s not too abstract that gets people there.

We should be able to do this in my book. This is going to be, obviously, later in the book. I haven’t gotten to it yet. This is level three, which is algebraic thinking. I don’t even have a good definition of it, a good explanation for it. This is one of those things where I’d love to get into discussions with people.

This is where you are focused on building models that compose nicely. Very few corner cases, is what I mean by nicely. They compose well. When you compose them, they have nice properties to them. You’re able to build a semantically complete system of interworking concepts.

You’re using everything from levels one and two to build something cohesive that operates in the abstract concepts of your domain.

When you’re talking about a data transformation pipeline, you’re often looking at, “OK, I’m getting this CSV and it’s got these values in it. I need to change it into something I could send to this JSON:API.” It’s slightly different format so I got to transform it.

You’re thinking very mechanical, very data-specific. What are the steps that I can go from this thing that I have to this thing that I need? It’s very important to do, but I’m talking about something like being able to build a…

Let’s say you’re making a video editor. Now, you’re going to want to model how the video editor will work on the backend. What are the concepts? How do they fit together so that they create a cohesive whole?

You can think of it as I should be able to concatenate two segments of the video. I should be able to cut a segment of the video. That means I’m going to have to model the segment. I’m going to have to model this operation of concatenating, which will give me a new segment, but it’s got the two combined in it.

I should be able to cut, which gives me the two segments, the before and the after segment. There’s a reasoning about the operations on these things, and how those operations can compose together to build the functionality that you will eventually need in your app.

Those are my three levels. Like I said several times already, these are a work-in-progress. Let me know what you think. Is there a fourth level that I’m missing? I haven’t reached the end of my functional programming journey so I’m probably not aware of what comes after this.

I’m definitely myself in three. I know other people there and I know people in the other two. I have some anecdotal evidence that this thing makes sense. I’m going to wrap it up. I’ll recap the three levels that I’m identifying mostly to organize my curriculum in the book so that there’s a sense of progress.

One is the distinction between actions, calculations, and data, and how to use them effectively. Two is higher-order thinking. This is where you’re using first-class functions, where people start to talk about data transformation pipelines, things like that.

Three is algebraic thinking. This is where you’re doing domain modeling at the operation level to be able to create a cohesive domain model in functions.

If you want to find all the old episodes, all future episodes, and this present episode, go to lispcast.com/podcast, and there you’ll find a list of all the episodes with video, audio, and text transcripts. You’ll also find links to subscribe and links to my social media so that we can get in touch if you want to do that.

I love having discussions, and I would love to have something like this more fleshed out based on a more solid ground. Awesome. My name is Eric Normand, and rock on.

The post The 3 levels of functional thinking appeared first on LispCast.

Permalink

Return Maps

Most Datomic queries return tuples, but sometimes you just want maps.  Today's release of the Datomic Cloud client library adds return maps to Datomic datalog.  For example, the following query uses the new :keys clause to request maps with :artist and :release keys:

[:find ?artist-name ?release-name
:keys artist release
:where [?release :release/name ?release-name]
[?release :release/artists ?artist]
[?artist :artist/name ?artist-name]]

Running against the mbrainz-sample database, this query returns:

#{{:artist "George Jones" :release "With Love"}
{:artist "Shocking Blue" :release "Hello Darkness / Pickin' Tomatoes"}
{:artist "Junipher Greene" :release "Friendship"}
...}

To try return maps, you can


Permalink

Playing With Graal VM

Recently, I started playing with Graal VM. What is Graal VM? To quote their website directly:

GraalVM is a universal virtual machine for running applications written in JavaScript, Python, Ruby, R, JVM-based languages like Java, Scala, Kotlin, Clojure, and LLVM-based languages such as C and C++.

Permalink

Copyright © 2009, Planet Clojure. No rights reserved.
Planet Clojure is maintained by Baishamapayan Ghose.
Clojure and the Clojure logo are Copyright © 2008-2009, Rich Hickey.
Theme by Brajeshwar.