Coder Social home page Coder Social logo

squint-cljs / squint Goto Github PK

View Code? Open in Web Editor NEW
568.0 15.0 34.0 1.72 MB

Light-weight ClojureScript dialect

Home Page: https://squint-cljs.github.io/squint

JavaScript 18.03% HTML 8.40% Emacs Lisp 0.02% Clojure 73.55% CSS 0.01%
clojure clojurescript javascript

squint's Introduction

Squint

Squint is a light-weight dialect of ClojureScript with a compiler and standard library.

Squint is not intended as a full replacement for ClojureScript but as a tool to target JS when you need something more light-weight in terms of interop and bundle size. The most significant different with CLJS is that squint uses only built-in JS data structures. Squint's output is designed to work well with ES modules.

If you want to use squint, but with the normal ClojureScript standard library and data structures, check out cherry.

โš ๏ธ This project is a work in progress and may still undergo breaking changes.

Quickstart

Although it's early days, you're welcome to try out squint and submit issues.

$ mkdir squint-test && cd squint-test
$ npm init -y
$ npm install squint-cljs@latest

Create a .cljs file, e.g. example.cljs:

(ns example
  (:require ["fs" :as fs]
            ["url" :refer [fileURLToPath]]))

(println (fs/existsSync (fileURLToPath js/import.meta.url)))

(defn foo [{:keys [a b c]}]
  (+ a b c))

(println (foo {:a 1 :b 2 :c 3}))

Then compile and run (run does both):

$ npx squint run example.cljs
true
6

Run npx squint --help to see all command line options.

Why Squint

Squint lets you write CLJS syntax but emits small JS output, while still having parts of the CLJS standard library available (ported to mutable data structures, so with caveats). This may work especially well for projects e.g. that you'd like to deploy on CloudFlare workers, node scripts, Github actions, etc. that need the extra performance, startup time and/or small bundle size.

Talk

ClojureScript re-imagined at Dutch Clojure Days 2022

(slides)

Differences with ClojureScript

  • The CLJS standard library is replaced with "squint-cljs/core.js", a smaller re-implemented subset
  • Keywords are translated into strings
  • Maps, sequences and vectors are represented as mutable objects and arrays
  • Standard library functions never mutate arguments if the CLJS counterpart do not do so. Instead, shallow cloning is used to produce new values, a pattern that JS developers nowadays use all the time: const x = [...y];
  • Most functions return arrays, objects or Symbol.iterator, not custom data structures
  • Functions like map, filter, etc. produce lazy iterable values but their results are not cached. If side effects are used in combination with laziness, it's recommended to realize the lazy value using vec on function boundaries. You can detect re-usage of lazy values by calling warn-on-lazy-reusage!.
  • Supports async/await:(def x (js-await y)). Async functions must be marked with ^:async: (defn ^:async foo []).
  • assoc!, dissoc!, conj!, etc. perform in place mutation on objects
  • assoc, dissoc, conj, etc. return a new shallow copy of objects
  • println is a synonym for console.log
  • pr-str and prn coerce values to a string using JSON.stringify (currently, this may change)

If you are looking for closer ClojureScript semantics, take a look at Cherry ๐Ÿ’.

Articles

Projects using squint

Advent of Code

Solve Advent of Code puzzles with squint here.

Seqs

Squint does not implement Clojure seqs. Instead it uses the JavaScript iteration protocols to work with collections. What this means in practice is the following:

  • seq takes a collection and returns an Iterable of that collection, or nil if it's empty
  • iterable takes a collection and returns an Iterable of that collection, even if it's empty
  • seqable? can be used to check if you can call either one

Most collections are iterable already, so seq and iterable will simply return them; an exception are objects created via {:a 1}, where seq and iterable will return the result of Object.entries.

first, rest, map, reduce et al. call iterable on the collection before processing, and functions that typically return seqs instead return an array of the results.

Memory usage

With respect to memory usage:

  • Lazy seqs in squint are built on generators. They do not cache their results, so every time they are consumed, they are re-calculated from scratch.
  • Lazy seq function results hold on to their input, so if the input contains resources that should be garbage collected, it is recommended to limit their scope and convert their results to arrays when leaving the scope:
(js/global.gc)

(println (js/process.memoryUsage))

(defn doit []
  (let [x [(-> (new Array 10000000)
               (.fill 0)) :foo :bar]
        ;; Big array `x` is still being held on to by `y`:
        y (rest x)]
    (println (js/process.memoryUsage))
    (vec y)))

(println (doit))

(js/global.gc)
;; Note that big array is garbage collected now:
(println (js/process.memoryUsage))

Run the above program with node --expose-gc ./node_cli mem.cljs

JSX

You can produce JSX syntax using the #jsx tag:

#jsx [:div "Hello"]

produces:

<div>Hello</div>

and outputs the .jsx extension automatically.

You can use Clojure expressions within #jsx expressions:

(let [x 1] #jsx [:div (inc x)])

Note that when using a Clojure expression, you escape the JSX context so when you need to return more JSX, use the #jsx once again:

(let [x 1]
  #jsx [:div
         (if (odd? x)
           #jsx [:span "Odd"]
           #jsx [:span "Even"])])

To pass props, you can use :&:

(let [props {:a 1}]
  #jsx [App {:& props}])

See an example of an application using JSX here (source).

Play with JSX non the playground

HTML

Similar to JSX, squint allows you to produce HTML as strings using hiccup notation:

(def my-html #html [:div "Hello"])

will set the my-html variable to a string equal to <div>Hello</div>.

Using metadata you can modify the tag function, e.g. to use this together with lit-html:

(ns my-app
  (:require ["lit" :as lit]))

#html ^lit/html [:div "Hello"]

This will produce:

lit/html`<div>Hello</div>`

See this playground example for a full example.

Async/await

Squint supports async/await:

(defn ^:async foo [] (js/Promise.resolve 10))

(def x (js-await (foo)))

(println x) ;;=> 10

Anonymous functions must have ^:async on the fn symbol or the function's name:

(^:async fn [] (js-await {}) 3)

Generator functions

Generator functions must be marked with ^:gen:

(defn ^:gen foo []
  (js-yield 1)
  (js-yield* [2 3])
  (let [x (inc 3)]
    (yield x)))

(vec (foo)) ;;=> [1 2 3 4]

Anonymous functions must have ^:gen on the argument vector:

(^:gen fn [] (js-yield 1) (js-yield 2))

See the playground for an example.

Arrow functions

If for some reason you need to emit arrow functions () => ... rather than function, you can use :=> metadata on the function expression, fn symbol or argument vector:

(fn ^:=> [] 1)

Defclass

See doc/defclass.md.

JS API

The JavaScript API exposes the compileString function:

import { compileString } from 'squint-cljs';

const f = eval(compileString("(fn [] 1)"
                             , {"context": "expr",
                                "elide-imports": true}
                            ));

console.log(f()); // prints 1

REPL

Squint has a console repl which can be started with squint repl.

nREPL

A (currently immature!) nREPL implementation can be used on Node.js with:

squint nrepl-server :port 1888

Please try it out and file issues so it can be improved.

Emacs

You can use this together with inf-clojure in emacs as follows:

In a .cljs buffer, type M-x inf-clojure. Then enter the startup command npx squint repl (or bunx --bun repl) and select the clojure or babashka type REPL. REPL away!

Truthiness

Squint respect CLJS truth semantics: only null, undefined and false are non-truthy, 0 and "" are truthy.

Macros

To load macros, add a squint.edn file in the root of your project with {:paths ["src-squint"]} that describes where to find your macro files. Macros are written in .cljs or .cljc files and are executed using SCI.

The following searches for a foo/macros.cljc file in the :paths described in squint.edn.

(ns foo (:require-macros [foo.macros :refer [my-macro]]))

(my-macro 1 2 3)

squint.edn

In squint.edn you can describe the following options:

  • :paths: the source paths to search for files. At the moment, only .cljc and .cljs are supported.
  • :extension: the preferred extension to output, which defaults to .mjs, but can be set to .jsx for React(-like) projects.
  • :copy-resources: a set of keywords that represent file extensions of files that should be copied over from source paths. E.g. :css, :json. Strings may also be used which represent regexes which are processed through re-find.

See examples/vite-react for an example project which uses a squint.edn.

Watch

Run npx squint watch to watch the source directories described in squint.edn and they will be (re-)compiled whenever they change. See examples/vite-react for an example project which uses this.

Svelte

A svelte pre-processor for squint can be found here.

Vite

See examples/vite-react.

React Native (Expo)

See examples/expo-react-native.

Compile on a server, use in a browser

See examples/babashka/index.clj.

Playground

License

Squint is licensed under the EPL. See epl-v10.html in the root directory for more information.

squint's People

Contributors

borkdude avatar arohner avatar lilactown avatar prabhjots avatar scottjad avatar brandonstubbs avatar pez avatar corasaurus-hex avatar hugoduncan avatar christophermaier avatar philbaker avatar purcell avatar github-actions[bot] avatar armincerf avatar ibdknox avatar wearethebork avatar djblue avatar chr15m avatar kigiri avatar jackdbd avatar jackrusher avatar mk avatar mjmeintjes avatar prestancedesign avatar pyr avatar shivekkhurana avatar wilhelmberggren avatar ikappaki avatar shaunlebron avatar sher avatar

Stargazers

Jacob Nguyen avatar Jonas Hvid avatar Filipe Guerreiro avatar Andrey Bogoyavlenskiy avatar Alenzo Manath avatar Brian avatar  avatar molety avatar  avatar Marco Dalla Stella avatar  avatar Jonathon McKitrick avatar Aaron B avatar kaiuri avatar Ibrahem Khalil avatar Kyle Grierson avatar Andrea Tupini avatar Camille Maussang avatar Ryan Martin avatar KevinLi avatar Adrian Medina avatar D W avatar cueaz avatar ErtuฤŸrul ร‡etin avatar Teodor Heggelund avatar Bobby avatar  avatar Patrick Delaney avatar Love Lagerkvist avatar Shin, SJ avatar tlonist-sang avatar Lalit Prakash Vatsal avatar Vincent avatar Felix Wittmann avatar Gleb Voropaev avatar Sergii Gulenok avatar Masoud Ghorbani avatar Nik Peric avatar Joe avatar  avatar Volodymyr Kotushenko avatar  avatar Stephan Renatus avatar ๊น€ํƒœํฌ avatar Vincent Olesen avatar lambdadbmal avatar David Lopez Jr. avatar Risto Stevcev avatar Rintaro Okamura avatar D. Bohdan avatar Bjorn Pagen avatar Konstantinos avatar a13ph avatar Zachary Romero avatar  avatar yfeng avatar Peter Yanovich avatar Siim Bobkov avatar macmoebius avatar Tatsunori Yoshioka avatar Ella Hoeppner avatar Lin ZiHao avatar Adam King avatar  avatar Joรฃo Paquim avatar  avatar Mikhail Kuzmin avatar Eric Lee avatar  avatar  avatar Julius Kibunjia avatar Vegard Steen avatar  avatar Taehee Kim avatar  avatar  avatar ksb avatar ninjure avatar  avatar Stoica George-Ovidiu avatar Jarkko Saltiola avatar merik avatar Kyle Allan avatar Bruno Heridet avatar The 99's Puppycat avatar Aliaksandr avatar Fredrik Holmqvist avatar Bรธrge Andrรฉ Jensen avatar Avi Flax avatar Volodymyr Vitvitskyi avatar  avatar Daniel Szmulewicz avatar  avatar Abhinav Omprakash avatar  avatar atsuo yamada avatar Sergey Smyshlyaev avatar JosephS avatar Gas avatar Dave Lynch avatar

Watchers

Abhik Khanra avatar Martin Klepsch avatar  avatar Vitaly Kravtsov avatar GuruPoo avatar  avatar Damon Kwok avatar Rafaล‚ Krzywaลผnia avatar  avatar Ruixiang Zhang avatar Dajanah avatar  avatar  avatar  avatar Harun Gรถzek avatar

squint's Issues

Core fn: sort

Implement sort

See #22 for implementation details of sequence functions

Core fn: repeat

Implement repeat

See #22 for implementation details of sequence functions

Core fn: drop

Implement drop, ignoring transducer arity (see #41 for why)

See #22 for implementation details of sequence functions

Core fn: take-nth

Implement take-nth, ignoring transducer arity (see #41 for why)

See #22 for implementation details of sequence functions

Core fn: keep

Implement keep, ignoring transducer arity (see #41 for why)

See #22 for implementation details of sequence functions

Core fn: take

Implement take, ignoring transducer arity (see #41 for why)

See #22 for implementation details of sequence functions

prn?(-str)?

Soooo pr-str/prn-str shoud just convert to JSON, right? And pr/prn should output to console.log?

Seqs & iterators

Background

The Clojure "seq" is a key concept in its language design. It provides a common abstraction for working with all kinds of collections where you want to treat them as an ordered sequence of elements.

JS has a similar abstraction called Iterators. Like a collection in Clojure can be "seqable," a collection in JS can be "iterable." Like seqs, iterators can lazily generate each element or be a view on top of an eager collection. The Iterator & Iterable protocols in JS underpin fundamental operations like for...of, similar to how Clojure seqs underpin map, filter et al. There also exist ways to create custom Iterators in JS using generators, just like in Clojure you can construct lazy sequences via lazy-seq.

Currently, iterators do not have any operations on them except the commonly use for ... of comprehension. There is currently a stage 2 proposal for adding more helpers like .map, .filter, etc: https://github.com/tc39/proposal-iterator-helpers

The major difference between the two abstractions is that iterators are mutable. Consuming an element in the iteration by calling the .next() method mutates the iterator object in place. This makes passing a reference of an iterator to a function a somewhat dangerous thing, as you can't be sure whether or not it consumes it.

Proposal

I would propose two things. I am still thinking this through, so feedback welcome:

  1. core seq ops like map, filter, etc. should accept anything that is Iterable and return a concrete array type
  2. transducers can work with raw iterators, avoiding the overhead of converting to an array each stage

Rationale

Seqs work well in Clojure because collections are immutable, so an immutable view on top of them does not need to worry about the underlying collection changing. In JS we do not have this guarantee, and an immutable view on top of a mutable collection can lead to surprising behavior. Clava also has a general philosophy of "use the platform" rather than bringing over Clojure abstractions. All of this is why I think basing our sequential operators on Iterators makes more sense than porting seqs.

In the above section, I proposed that we take anything that is Iterable but always return an array in map, filter, etc. The alternative would be to return an Iterator directly, similar to the TC39 proposal I linked in the background. However, I think this would lead to a lot of confusion when writing application code. Take the following example:

(defn my-todo-app
  (let [[todos set-todos] (React/useState [{:name "Task 1" :completed false} {:name "Task 2" :completed false}])
        todos-with-id (map-indexed #(assoc %2 :id %1) todos)
        todo-count (count todos-with-id)]
      [:div
       (for [todo todos-with-id]
         [:div {:key (:id todo)} (:name todo)])]))

If map-indexed returns an Iterator, this code is broken: the (count todos-with-id) will consume the iterator, which will lead to nothing being in the Iterator when React tries to render the divs. Developers will have to be careful in their application code to only use the result of a map, map-indexed or filter once. I think that this is an unreasonable thing to force developers to keep in mind, which is why I think that we should do these operations eagerly and return an array.

For transducers, however, we can own the Iterator as long as it's in the transducer pipeline. So code like:

(into [] (comp (map inc) (filter even?)) (range 10))

(range 10) could return an Iterable (e.g. an array). The transducer pipeline can pass an iterator to each stage, avoiding the cost of creating an array each time, before finally adding it all to the array passed to into.

Gotta stop here. Will add more thoughts later. Questions and comments welcome!!

Core fn: mapcat

Implement mapcat, ignoring transducer arity (see #41 for why)

See #22 for implementation details of sequence functions

Core fn: drop-while

Implement drop-while, ignoring transducer arity (see #41 for why)

See #22 for implementation details of sequence functions

Core fn: every?

Implement every

See #22 for implementation details of sequence functions

Core fn: concat

Implement concat

See #22 for implementation details of sequence functions

Consider making JS-consumable API (assocIn instead of assoc_in, etc)

What should be the JS name for assoc_BANG_?
Should we re-export the beautified names in another module?
What about using these functions from a CLJS project, then we don't need the renaming?

Alternative:

People could compile a script like this:

(def assocIn clava.assoc-in)

and then consume that module.

Hmm, it might actually be a feature that we're using the Clojure convention since you can then import these functions in a real CLJS project:

function foo_bar() {
  return 10;
}

exports.foo_bar = foo_bar;

(foo/foo-bar)

Maps & complex keys

Currently, clavascript maps are compiled to JS objects. This works fine as long until you need complex keys, e.g.

{[:foo "123"] {:foo/id "123" :bar "baz"}}

In the above case, the vector is used as a key. JS objects only support string keys.

Core fn: distinct

Implement distinct, ignoring transducer arity (see #41 for why)

See #22 for implementation details of sequence functions

Core fn: take-while

Implement take-while, ignoring transducer arity (see #41 for why)

See #22 for implementation details of sequence functions

Core fn: some

Implement some

See #22 for implementation details of sequence functions

Macroify: nth

nth should emit code the same as unchecked-get:

(nth [1 2 3] 2)
([1, 2, 3][2])

Records & Tuples

One complaint I heard in the cherry channel was how clojurescript had fallen behind with interop. Given that, it might make sense to plan for likely js features coming down the road so that interop doesn't fall behind.

One of these features that I think will be important is the proposal for Records & Tuples: https://tc39.es/proposal-record-tuple/tutorial/

I'm not sure we need to do anything right now but keeping it in mind for designing interop in general seems like a smart move to me.

We can probably close this issue right away but if we want to discuss then this is a good place to do it.

Collection functions

Thinking about fundamental functions on collections like assoc, dissoc, conj, disj, it would be nice to have these work on JS objects in an immutable way.

assoc

A common idiom in JS is to use copy-on-write. E.g.

let o1 = {a: 1, b: 2, c: 3};

let o2 = {d: 4, ...o1}; // copy all enumerable properties of o1 into o2

assoc could be a core function that does that

export function assoc(o, ...pairs) {
  let o2 = {...o};
  // TODO add pairs to new object o2
  return o2;
};

alternatively, it could be a special form in the compiler and literally emit the object spread syntax above.

dissoc

dissoc can also be done via object spread:

let o = {a: 1, b: 2, c: 3};
{a, ...o2} = o; // o2 is `{b: 2, c:3}`

To avoid shadowing variables that have the same name as the property we're removing, this should probably be a function in core that encapsulates this.

conj

conj could be added via prototype to objects, sets, and arrays. However this is generally frowned upon because a browser could later implement whatever method we add to it. There are ways around this (using Symbol), however there still could maybe be hazards with this if at some point before we add the method those prototypes are frozen (would this ever happen? I'm not sure).

A general approach would be to dispatch on the prototype object, and fallback to a method on the object:

let object = Object.getPrototypeOf({});
let array = Object.getPrototypeOf([]);
let set = Object.getPrototypeOf(new Set());

export function conj(o, x) {
  switch (Object.getPrototypeOf(o)) {
    case object: ...
    case array: ...
    case set: ...
    default:
      o.conj(x);
  }
}

disj

Probably same as conj but only special casing sets

with-out-str

Is this something we want to add? If we do want to add this then we should probably do this before we do pr/prn, right?

String interpolation

@corasaurus-hex String interpolation is a different issue: this issue is just about generating more efficient code for (str foo bar) => cherry/str(foo,bar) vs foo + bar.
I think string interpolation should also be a built-in feature that compiles directly to JS string interpolation. We can support this using a reader template #i "foo ${name}". I'll make a different issue for that.

Originally posted by @borkdude in #10 (comment)

JS Sets

More of a discussion than an issue: Should set literals create JS Set objects?

Core fn: interpose

Implement interpose, ignoring transducer arity (see #41 for why)

See #22 for implementation details of sequence functions

equal? predicate for structural equality

We could support (ns foo (:require [clava.core :refer [equal?]])) as a synonym for (:require ["clavascript/core.js" :refer [equal?]]).

equal? works on nested arrays, objects and sets and falls back to === for other types.

When comparing two objects, first it should be determined if they are of the same type using instanceof (or so) and then we should defer to helper functions which efficiently check the contents of two things of the same type, e.g. arrayEquals, setEquals. A quick first check is to check if the length of the two objects are the same, before traversing the object.

Transducers

Background

Moving this from slack to an issue.

In Clojure, transducers are created by calling core seq functions without the collection, e.g. (map inc), (filter even?). While convenient (no new names needed, no new requires needed), it also adds a couple of challenges:

  1. Programming errors emerge that aren't caught by simple arity checking, e.g. (first (map inc))
  2. Transducer code can't be tree shaken if one uses (map inc coll)

The first can be caught by better static analysis (clj-kondo) but the second is hard for existing tooling to do.

I proposed two different provocative solutions in slack:

  • what if we only had transducers, and eschewed map filter with the collection passed in?
  • what if we moved transducers to its own namespace?

Initial response to the first was that it would be too unfamiliar to people coming from Clojure(Script), but that the second would be a good one to try.

Proposal

We add a new namespace, clava.transducers which would hold the transducer protocol and transducing functions.

Transducers would be objects that satisfy the transformer protocol, allowing them to interop with other libraries like Ramda.

We could build it in JS, however I'm interested in trying to build it in Clava source. This would necessarily force us to build and exercise #21.

Feedback welcome!

Destructuring nil and undefined

Currently the following fails:

(let [[foo bar] nil]
  foo)

This currently emits calls to nth, which tries to access the indices on the null value.

I think we can fix this by special casing null in nth.

Semi-related to #9

Symbols

Should Clojure symbols map to Symbols, some custom type, or strings like keywords?

Macroify: str

str should emit "" + ... code.

I think we still need the core str function when the common (apply str ,,,) is used

equality

What should (= x y) compile into?

Core fn: last

Implement last

See #22 for implementation details of sequence functions

Protocols

Right now, protocols generate a LOT of code. This makes it sort of hard to justify using it for a lot of standard library stuff.

In JS practice, protocol-like extensions are done using Symbols that are added to objects as properties. The symbols allow one to guarantee that there will be no conflicts with other code extending it.

The way I think this could work is like the following. For code like:

(defprotocol IFoo
  (bar [this])
  (baz [this a])
  (baz [this a b])
  (baz [this a b & more]))

It would generate the following code:

var IFoo = Symbol.for("my.ns.IFoo");
var IFoo_bar = Symbol.for("my.ns.IFoo/bar");
var IFoo_baz = Symbol.for("my.ns.IFoo/baz");

function bar(o) {
  assert_method(o, IFoo_bar);
  return o[IFoo_bar](o);
}

function baz(o, ...args) {
  assert_method(o, IFoo_baz);
  return o[IFoo_baz].apply(o, o, args);
}

Extension would mutate the object, adding symbol as a property:

(extend-type MyClass
  IFoo
  (bar [mc] :bar)
  (baz [mc a] [:baz a])
  (baz [mc a b] [:baz a b])
  (baz [mc a b & more] (into [:baz a b] more))
MyClass[IFoo] = true;
MyClass[IFoo_bar] = function (mc, a) { return ["baz", a]; };
MyClass[IFoo_baz] = function (mc, ...args) {
  if (args.length === 1) {
    return ["baz", args[0]];
  } else if (args.length === 2) {
    return ["baz", args[0], args[1]];
  } else if (args.length > 2) {
    let [a, b, ...more] = args;
    return into(["baz", a, b], more);
  } else {
    throw new Error("Invalid arity: " + args.length);
  }
}

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.