Code Monkey home page Code Monkey logo

exemplar's Introduction

Exemplar

CircleCI codecov Clojars Project

Record functions.

Show me what you got. I want to see what you got.

  • Teddy Roosevelt

Fake data

Things like type systems allow us to describe data in general terms. But what happens when we need instances of those descriptions and we have none? Typically we make something up so we can continue writing software until the real data arrives. We use fake data. Despite the importance it plays in getting software built, it's not something we talk about much.

How do we get fake data? Sometimes we write it out by hand. Other times we write code that's sole purpose is to generate it. We copy and paste the result of database queries or API calls. We put the world into particular state, log whatever it is we're looking for, and copy and paste it into our workspace.

However we do it, we spend significant time and effort obtaining and working with fake data. Yet, unless it makes it into a test, we throw it away.

What if we didn't do that?

(ns my-ns
  (:require [exemplar.core :refer :all])
 
(spit "my-file.edn" {})
(register-path "my-file.edn")

(defn my-function [a b c] (+ a b c))

(save (my-function 1 2 3))
=> 6

(show my-function)
=> {:name my-function
    :ns my-ns
    :in [1 2 3]
    :out 6
    :source "(defn my-function [a b c] (+ a b c))"}

(run my-function)
=> 6

Why keep fake data?

It can be nice to see an example of the input a function was designed to receive. This can be especially helpful in cases where the input to a function is not defined by a spec. If obtaining fake data is something we'd like to do less of, saving it might prevent us from recreating it later. Even when we change the input a function receives, having an example of the old input might be helpful. We might be able to slightly modify the old example input to produce the new.

What else could we do with persisted mock data? We can generate tests. For pure functions, this is especially trivial.

We can generate documentation that includes example inputs and outputs.

And if you're already using fake data in development, this should all be free.

Recording a running application

Exemplar can record the same information about a function as an application is running. Functions can be recorded once or until an explicit call to stop-recording. Persisted entries for functions are overwritten. The data persisted is always that of the last call, so you won't accumulate 1,000 example inputs and outputs per function.

Record once:

(ns my-ns
  (:require [exemplar.core :refer :all])
 
(spit "my-file.edn" {})
(register-path "my-file.edn")

(defn my-function [a b c] (+ a b c))

(record-once my-function)

(my-function 1 2 3)
=> 6

(show my-function)
=> {:name my-function
    :ns my-ns
    :in [1 2 3]
    :out 6
    :source "(defn my-function [a b c] (+ a b c))"}

(my-function 2 3 4)
=> 9

(show my-function)
=> {:name my-function
    :ns my-ns
    :in [1 2 3]
    :out 6
    :source "(defn my-function [a b c] (+ a b c))"}

Record until stop:

(ns my-ns
  (:require [exemplar.core :refer :all])
 
(spit "my-file.edn" {})
(register-path "my-file.edn")

(defn my-function [a b c] (+ a b c))

(record my-function)

(my-function 1 2 3)
=> 6

(show my-function)
=> {:name my-function
    :ns my-ns
    :in [1 2 3]
    :out 6
    :source "(defn my-function [a b c] (+ a b c))"}

(my-function 2 3 4)
=> 9

(show my-function)
=> {:name my-function
    :ns my-ns
    :in [2 3 4]
    :out 9
    :source "(defn my-function [a b c] (+ a b c))"}
    
(stop-recording my-function)

(my-function 1 2 3)
=> 6

(show my-function)
=> {:name my-function
    :ns my-ns
    :in [2 3 4]
    :out 9
    :source "(defn my-function [a b c] (+ a b c))"}

For convenience, you can also record entire namespaces.

(ns my-ns
  (:require [exemplar.core :refer :all])
 
(spit "my-file.edn" {})
(register-path "my-file.edn")

(defn my-function [a b c] (+ a b c))
(defn my-other-function [xs] (map inc xs))
(def some-def {:cool true)
(defmacro some-macro [sym] `[~sym])

(record-namespace my-ns)

(my-function 1 2 3)
=> 6
(my-other-function [1 2 3])
=> (2 3 4)

(show my-function)
=> {:name my-function
    :ns my-ns
    :in [1 2 3]
    :out 6
    :source "(defn my-function [a b c] (+ a b c))"}

(show my-other-function)
=> {:name my-other-function
    :ns my-ns
    :in [[1 2 3]]
    :out (2 3 4)
    :source "(defn my-other-function [xs] (map inc xs))"}
    
(my-function 2 3 4)
=> 9
(my-other-function [2 3 4])
=> (3 4 5)

(show my-function)
=> {:name my-function
    :ns my-ns
    :in [1 2 3]
    :out 6
    :source "(defn my-function [a b c] (+ a b c))"}
    
(show my-other-function)
=> {:name my-other-function
    :ns my-ns
    :in [[1 2 3]]
    :out (2 3 4)
    :source "(defn my-other-function [xs] (map inc xs))"}
    
(stop-recording-namespace my-ns)

(my-function 3 4 5)
=> 12
(my-other-function [3 4 5])
=> (4 5 6)

(show my-function)
=> {:name my-function
    :ns my-ns
    :in [1 2 3]
    :out 6
    :source "(defn my-function [a b c] (+ a b c))"}
    
(show my-other-function)
=> {:name my-other-function
    :ns my-ns
    :in [[1 2 3]]
    :out (2 3 4)
    :source "(defn my-other-function [xs] (map inc xs))"}

Generate tests

  1. Ensure you've registered the path to your .edn file and recorded the function:
(exemplar/register-path "test.edn")
(defn my-func [xs] (map inc xs))
(exemplar/save (my-func [1 2 3])
  1. Generate a test file (if one doesn't exist)
(init-test-ns 'my-generated-tests "test" ["my-proj"])
;; Creates `test/my-proj/my_generated_tests.clj` with namespace `my-proj.my-generated-tests`
  1. Generate a test
(exemplar/write-test my-func "test/my-proj/my_generated_tests.clj")

"test/my-proj/my_generated_tests.clj":

(ns my-proj.my-generated-test 
  (:require [clojure.test :refer [deftest is testing]]
                  [my-proj.core]))
 
 
(deftest my-proj-core-my-func-test
  (is (= (apply my-proj.core/my-func [[1 2 3]])
         '(2 3 4))))

FAQ

You're recording all inputs and outputs to a function?

We do record them but overwrite each call on disk and in memory. So you only get to see the last one. We will probably add an option to persist each call but we are only one person at the time of this writing.

Why doesn't it work from a REPL?

You can use a REPL but the function to be recorded or saved must be written in a file. We read from a file as part of saving and recording.

Why is my persisted function definition wrong?

The line numbers in the REPL you're running don't correspond to what's on disk. Restarting the REPL should fix it.

Does it work with macros?

No. We haven't tried yet. It might be difficult or impossible, but we plan to try to support them (we use them a lot). Community input helps us prioritize, so let us know if it's a feature you'd like to see.

Does it work with impure functions?

Yes, but there's no magic to how we currently handle them. We don't know anything about the atoms or vars an impure function might reference. A function that performs a side effect and returns nil will have that its output. A function that performs a computation with a value that changes over time will have that reflected as its output. If there's interest we can probably do more here. I haven't hit a limit of what's possible with Clojure yet.

Goals

  • Have fun
  • Generate tests
  • Generate documentation
  • Allow accumulation of inputs and outputs instead of overwriting

Acknowledgements

License

Apache 2.0

exemplar's People

Contributors

alex-dixon avatar timothypratley avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Forkers

timothypratley

exemplar's Issues

NPE when using to record other lib

local-file lib provides an absolute path to a source file when we run outside a jar but nil when inside a jar.

Offending fn is ‘ns->abs-path’.

Goal is to to get a file that contains code from a namespace object and to be able to read the file.

No reader function for tag object

Writing an instance of java.File to an EDN file succeeds, but an error is thrown when reading the file back. This can occur both in the :in and :out keys, both of which are written out as EDN/Clojure.

Some thoughts:

  • Decide whether it's important to be able to read something like a file back, and the feasibility of this. A file might be a special case, since it could contain a relative path and so work in some contexts but not others. Other objects that are not serialized by default should not have this issue.
  • Note: we can simply provide a string view of the output and the input, but this alone will prevent us from being able to say (:in (exemplar/show my-function)) and work with the input that we have saved in a natural fashion. That would solve the case though of writing something that is unreadable, then failing on subsequent reads (which happen during write operations so we can merge what we're writing out). It would in other words isolate the issue so that it would only be encountered when user attempts to to use the saved input from the REPL that is unreadable.
  • Figure out whether records can be serialized and deserialized with the current setup. It's really important we support them.
  • Figure out at what stage we want to enhance persistence support for faster serialization, deserialization, smaller storage, and backends like AWS etc. Came across this that might help https://github.com/jimpil/duratom.
  • Bear in mind the output is meant to be human readable. We want that to be as easy as possible. Right now a user can open up the EDN file and the data is human readable 100 percent. We want to maintain this functionality and simplicity of use if at all possible.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.