Coder Social home page Coder Social logo

lambdageek / unbound-generics Goto Github PK

View Code? Open in Web Editor NEW
55.0 5.0 18.0 305 KB

Specify variable binding in syntax trees using GHC.Generics (reimplementation of Unbound)

Home Page: https://hackage.haskell.org/package/unbound-generics/

License: BSD 3-Clause "New" or "Revised" License

Haskell 100.00%
ghc-generics variable-binding substitution bound unbound haskell-library

unbound-generics's Introduction

unbound-generics

Join the chat at https://gitter.im/lambdageek/unbound-generics Discord

Hackage CI

Support for programming with names and binders using GHC Generics.

Summary

Specify the binding structure of your data type with an expressive set of type combinators, and unbound-generics handles the rest! Automatically derives alpha-equivalence, free variable calculation, capture-avoiding substitution, and more. See Unbound.Generics.LocallyNameless to get started.

This is a reimplementation of (parts of) unbound but using GHC generics instead of RepLib.

Examples

Some examples are in the examples/ directory in the source. And also at unbound-generics on GitHub Pages

Example: Untyped lambda calculus interpreter

Here is how you would implement call by value evaluation for the untyped lambda calculus:

{-# LANGUAGE DeriveDataTypeable, DeriveGeneric, MultiParamTypeClasses #-}
module UntypedLambdaCalc where
import Unbound.Generics.LocallyNameless
import GHC.Generics (Generic)
import Data.Typeable (Typeable)

-- | Variables stand for expressions
type Var = Name Expr

-- | Expressions
data Expr = V Var                -- ^ variables
          | Lam (Bind Var Expr) -- ^ lambdas bind a variable within a body expression
          | App Expr Expr       -- ^ application
          deriving (Show, Generic, Typeable)

-- Automatically construct alpha equivalence, free variable computation and binding operations.
instance Alpha Expr

-- semi-automatically implement capture avoiding substitution of expressions for expressions
instance Subst Expr Expr where
  -- `isvar` identifies the variable case in your AST.
  isvar (V x) = Just (SubstName x)
  isvar _     = Nothing

-- evaluation takes an expression and returns a value while using a source of fresh names
eval :: Expr -> FreshM Expr
eval (V x) = error $ "unbound variable " ++ show x
eval e@Lam{} = return e
eval (App e1 e2) = do
  v1 <- eval e1
  v2 <- eval e2
  case v1 of
   Lam bnd -> do
     -- open the lambda by picking a fresh name for the bound variable x in body
     (x, body) <- unbind bnd
     let body' = subst x v2 body
     eval body'
   _ -> error "application of non-lambda"

example :: Expr
example =
  let x = s2n "x"
      y = s2n "y"
      e = Lam $ bind x (Lam $ bind y (App (V y) (V x)))
  in runFreshM $ eval (App (App e e) e)
  
-- >>> example
-- Lam (<y> App (V 0@0) (Lam (<x> Lam (<y> App (V 0@0) (V 1@0)))))

Differences from unbound

For the most part, I tried to keep the same methods with the same signatures. However there are a few differences.

  1. fv :: Alpha t => Fold t (Name n)

    The fv method returns a Fold (in the sense of the lens library), rather than an Unbound.Util.Collection instance. That means you will generally have to write toListOf fv t or some other summary operation.

  2. Utility methods in the Alpha class have different types.

    You should only notice this if you're implementing an instance of Alpha by hand (rather than by using the default generic instance).

    1. isPat :: Alpha t => t -> DisjointSet AnyName The original unbound returned a Maybe [AnyName] here with the same interpretation as DisjointSet: Nothing means an inconsistency was encountered, or Just the free variables of the pattern.
    2. isTerm :: Alpha t => t -> All
    3. open :: Alpha t => AlphaCtx -> NthPatFind -> t -> t, close :: Alpha t => AlphaCtx -> NamePatFind -> t -> t where NthPatFind and NamePatFind are newtypes
  3. embed :: IsEmbed e => Embedded e -> e and unembed :: IsEmbed e => e -> Embedded e

    The typeclass IsEmbed has an Iso (again in the sense of the lens library) as a method instead of the above pair of methods.

    Again, you should only notice this if you're implementing your own types that are instances of IsEmbed. The easiest thing to do is to use implement embedded = iso yourEmbed yourUnembed where iso comes from lens. (Although you can also implement it in terms of dimap if you don't want to depend on lens)

unbound-generics's People

Contributors

alex-mckenna avatar andreasabel avatar bergmark avatar byorgey avatar christiaanb avatar emilypi avatar erlandsona avatar gitter-badger avatar lambdageek avatar liesnikov avatar marklemay avatar noughtmare avatar sdiehl avatar sweirich avatar totbwf avatar

Stargazers

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

Watchers

 avatar  avatar  avatar  avatar  avatar

unbound-generics's Issues

subst captures variables when substitution is with a bound variable

as of 7ce8463 subst checks that variables substituted for are free but doesn't do anything to avoid capture

subst :: Name b -> b -> a -> a
default subst :: (Generic a, GSubst b (Rep a)) => Name b -> b -> a -> a
subst n u x =
if (isFreeName n)
then case (isvar x :: Maybe (SubstName a b)) of
Just (SubstName m) | m == n -> u
_ -> case (isCoerceVar x :: Maybe (SubstCoerce a b)) of
Just (SubstCoerce m f) | m == n -> maybe x id (f u)
_ -> to $ gsubst n u (from x)
else error $ "Cannot substitute for bound variable " ++ show n
substs :: [(Name b, b)] -> a -> a
default substs :: (Generic a, GSubst b (Rep a)) => [(Name b, b)] -> a -> a
substs ss x
| all (isFreeName . fst) ss =
case (isvar x :: Maybe (SubstName a b)) of
Just (SubstName m) | Just (_, u) <- find ((==m) . fst) ss -> u
_ -> case isCoerceVar x :: Maybe (SubstCoerce a b) of
Just (SubstCoerce m f) | Just (_, u) <- find ((==m) . fst) ss -> maybe x id (f u)
_ -> to $ gsubsts ss (from x)
| otherwise =
error $ "Cannot substitute for bound variable in: " ++ show (map fst ss)

this means that we take terms from pi-forall (https://github.com/sweirich/pi-forall/blob/729c9f4e348bd94090b0415e3391ad8bb1d89305/full/src/Syntax.hs) and run the following:

import Unbound.Generics.LocallyNameless.Name as Unbound

an :: TName
an = Unbound.string2Name "aname"

bn :: TName
bn= Unbound.string2Name "bname"

match = Match (Unbound.bind (PatCon trueName []) (Var bn))

bound :: TName
bound = Unbound.Bn 0 0 -- a bound variable

abound = Unbound.bind an (Case (Var an) [match])
{-|
>>> show abound 
<aname> Case (Var 0@0) [Match (<(PatCon "True" [])> Var bname)]

>>> show $ Unbound.subst bn (Var bound) $ abound
<aname> Case (Var 0@0) [Match (<(PatCon "True" [])> Var 0@0)]
-}

We see that Var 0@0 is captured by the case, the correct result would be

<aname> Case (Var 0@0) [Match (<(PatCon "True" [])> Var 1@0)]

Allow name representations other than String

The current representation of names is based on a String and a disambiguating integer.

data Name a = Fn String !Integer    -- free names
            | Bn !Integer !Integer  -- bound names / binding level + pattern index

Perhaps it would make sense to make names generic, such that other representations of the name (using Text, for instance), can be used. This would also allow things like attaching additional information to names (e.g., free variables might be tagged with their type).

I would propose a datatype like the following:

data Name f a = Fn f !Integer
              | Bn !Integer !Integer

This is roughly the way I have (manually) implemented binding code in some of my own projects. I also tend to have a separate data type for free variables, which removes the need to check whether a name is a free variable in places where only free variables are expected (for example, a typing context typically does not hold any bound variables), like so:

data Name f a = Name f !Integer

data Var f a = Fn (Name f a)
             | Bn !Integer !Integer

I'm not actually that familiar with the design constraints of unbound-generics, and there might be considerations that make the use of this representation undesirable. I would be happy to hear any thoughts on this, and would also be willing to spend some time to implement this change.

Cherrypick `instantiate` from sweirich's branch

See #16 (comment)

Pickup the new instantiate function:

-- | Immediately substitute for the bound variables of a pattern
-- in a binder, without first naming the variables.
-- NOTE: this operation does not check that the number of terms passed in 
-- match the number of variables in the pattern. (Or that they are of appropriate type.)
instantiate :: (Alpha a, Alpha b, Alpha p, Subst a b) => Bind p b -> [a] -> b

Support for capturing substitution

When dealing with program refinement (or really any domain with the notion of a meta-variable), capturing substitution is required. For example, consider the language and judgements below.

e ::= x (variables)
        | \x.e (abstraction)
        | e e (application)
        | ?x (meta-variable)
        | ?x.e (hole)
        | ?x ~ e.e (guess)


                     pure e = b           pure e1 = l    pure e2 = r
-----------------  ------------------     -----------------------------   -------------------
pure x = true       pure \x.e = b          pure e1 e2 = l ^ r                pure ?x = true


--------------------     -----------------------------
pure ?x.e = false        pure ?x ~ e1.e2 = false

           pure e1
--------------------------------        
solve ?x ~ e1.e2 = [e1/x]e2

Informally speaking, you can solve a hole if and only if the guess does not contain holes itself. However, the meta-variable substitution must capture variables. As a concrete example:
solve ?x ~ y. \y.?x = \y.y.

As it currently stands, the only way to implement capturing substitutions (and metavariables) that I have come upon is pretty hacky, and any other way would require support in unbound itself.

In my mind, there are 2 options:

  1. Add substCapturing :: AlphaCtx -> Name b -> b -> a -> a as a method on Subst
  2. Add support for meta-variables directly

Seeing as the only reasonable use that I can think of for substCapturing would be for implementing
meta-variables, I think that option 2 would be the best approach, but it has its downsides as well.

Either way, I can write up a PR to implement this if it seems like something that would be a useful addition.

Variant of `makeClosedAlpha` that works for type constructors.

So we can do

  data T = A | B | C deriving (Eq, Ord, Show)
  $(makeClosedAlpha ''T)

but we could also want

  data T a where
    A :: Int -> T Int
    B :: Bool -> T Bool
    C :: Char -> T Char
  deriving instance Eq (T a)
  deriving instance Ord (T a)
  deriving instance Show (T a)
  $(makeClosedAlpha ''T)

which doesn't work right now because it makes the following instance declaration

  instance Alpha T

instead of the kind-correct

  instance Alpha (T a)

Build failure with mtl-2.3

Building library for unbound-generics-0.4.1..
[ 1 of 19] Compiling Unbound.Generics.LocallyNameless.Internal.Fold ( src/Unbound/Generics/LocallyNameless/Internal/Fold.hs, dist/build/Unbound/Generics/LocallyNameless/Internal/Fold.o, dist/build/Unbound/Generics/LocallyNameless/Internal/Fold.dyn_o )
[ 2 of 19] Compiling Unbound.Generics.LocallyNameless.Internal.Iso ( src/Unbound/Generics/LocallyNameless/Internal/Iso.hs, dist/build/Unbound/Generics/LocallyNameless/Internal/Iso.o, dist/build/Unbound/Generics/LocallyNameless/Internal/Iso.dyn_o )
[ 3 of 19] Compiling Unbound.Generics.LocallyNameless.Internal.Lens ( src/Unbound/Generics/LocallyNameless/Internal/Lens.hs, dist/build/Unbound/Generics/LocallyNameless/Internal/Lens.o, dist/build/Unbound/Generics/LocallyNameless/Internal/Lens.dyn_o )
[ 4 of 19] Compiling Unbound.Generics.LocallyNameless.Name ( src/Unbound/Generics/LocallyNameless/Name.hs, dist/build/Unbound/Generics/LocallyNameless/Name.o, dist/build/Unbound/Generics/LocallyNameless/Name.dyn_o )
[ 5 of 19] Compiling Unbound.Generics.LocallyNameless.LFresh ( src/Unbound/Generics/LocallyNameless/LFresh.hs, dist/build/Unbound/Generics/LocallyNameless/LFresh.o, dist/build/Unbound/Generics/LocallyNameless/LFresh.dyn_o )

src/Unbound/Generics/LocallyNameless/LFresh.hs:116:7: error:
    Not in scope: type constructor or class ‘MonadPlus’
    |
116 |     , MonadPlus
    |       ^^^^^^^^^

src/Unbound/Generics/LocallyNameless/LFresh.hs:117:7: error:
    Not in scope: type constructor or class ‘MonadFix’
    Perhaps you meant ‘MonadFail’ (imported from Prelude)
    |
117 |     , MonadFix
    |       ^^^^^^^^

Implement a nominal version of the library.

In unbound Unbound.Nominal there is (the beginnings of) a version of the library that doesn't use a locally nameless representation, instead it just uses a simple nominal representation. @sweirich says that the idea was to investigate whether this version would perform better, but that the code is not worth porting as is.

If we decide to do this, one thing I'd look into is whether we can avoid having two completely unrelated Alpha type classes (harder), two different sets of binding combinators (easier) and even whether it's possible to mix named and nameless representations for two different sorts of names in a single project.

Some kind of local scope in pattern bindings

Consider implementing SML data type definitions:

   datatype 'a option = NONE | SOME of 'a

The whole data definition is a pattern in the rest of the module, but the variable a should scope locally over the value constructors which are themselves in scope in the rest of the module.

If we were working with terms, we'd just use a Bind to introduce a scope so that the a ranged over the value constructors.

But since NONE and SOME are constructor names that scope over the rest of the module, what we actually want is to use a LocalBind as a pattern. (This is in some sense a complement to Shift which is all about lifting terms out of scopes whereas this would be limiting the scope of a pattern)

Making Alpha instances for Opaque types is harder than with Unbound/RepLib

In RepLib there is a derive_abstract function that constructs a generic representation for opaque types. Practically that means that if there is some type that needs an Alpha instance solely because it's a leaf within some AST, it's sufficient to write

   derive_abstract ''MyType
   instance Alpha MyType where
     aeq _ = (==)
     acompare _ = compare

and the remaining methods will get a generic implementation that leaves the values untouched.

In unbound-generics, however if a type does not have a Generic instance, we have to write by hand implementations that do nothing.

instance Alpha MyType where
  aeq' _ctx = (==)
  fvAny' _ctx _nfn i = pure i
  close _ctx _b = id
  open _ctx _b = id
  isPat _ = mempty
  isTerm _ = True
  nthPatFind _ = Left
  namePatFind _ _ = Left 0
  swaps' _ctx _p = id
  freshen' _ctx i = return (i, mempty)
  lfreshen' _ctx i cont = cont i mempty

These are really verbose and kind of unpleasant. It would be nice if we could have some mechanism for constructing them automatically. I could imagine, for example making Alpha of kind Opacity -> * -> Constraint so that you then write

  instance Alpha Translucent MyAST

and there is a universal instance

  instance Alpha Opaque a where
     -- everything I wrote above for MyType

but I'm not sure if that's the best way.

SetBind, SetPlusBind...

Unbound/RepLib had these additional binders that allow you to express patterns where the bound names are treated in such a way that their order doesn't matter. (For example you could create a term local x=1, y=2 in e((x,y)) which is alpha-equivalent to local y=2, x=1 in e.)

It should be pretty straightforward to add this back in. All the magic is in the smart constructors setbind and setplusbind. (One would also need to generalize Bind to GenBind as in Unbound. No big deal)

Bump for GHC 8.6.1

I actually have a branch ghc-861-travis that bumps our upper bounds.

But I can't test it because I run into build issues on travis.

At the moment, I'm building with --allow-newer=base,containers,binary and it gets as far as rebuilding Cabal-2.2.0.1 (b/c reasons...) and then breaks because that version of Cabal uses a deprecated function from containers and containers-0.6 removed it:

Distribution/PackageDescription/Configuration.hs:222:39: error:
    • Data.Map.insertWith' is gone. Use Data.Map.Strict.insertWith.
    • In the first argument of ‘Map.foldrWithKey’, namely
        ‘(Map.insertWith' combine)’
      In the expression:
        Map.foldrWithKey
          (Map.insertWith' combine) (unDepMapUnion xs) (unDepMapUnion ys)
      In an equation for ‘union’:
          union
            = Map.foldrWithKey
                (Map.insertWith' combine) (unDepMapUnion xs) (unDepMapUnion ys)
    |
222 |         let union = Map.foldrWithKey (Map.insertWith' combine)
    |                                       ^^^^^^^^^^^^^^^^^^^^^^^
Failed to install Cabal-2.2.0.1
cabal: Error: some packages failed to install:
Cabal-2.2.0.1-FcabUjH9MHb6X9mJWWeEmL failed during the building phase. The
exception was:
ExitFailure 1
bifunctors-5.5.3-DKOZCSeXJl2DeGxgt9KwMz depends on bifunctors-5.5.3 which
failed to install.
cabal-doctest-1.0.6-4W568SY7EO64yyE1vbTC6r depends on cabal-doctest-1.0.6
which failed to install.
comonad-5.0.4-E8iDC86E3q2Bfy0bXvSixA depends on comonad-5.0.4 which failed to
install.
distributive-0.6-KKdNmBTJnxYKhiwNjoFylh depends on distributive-0.6 which
failed to install.
profunctors-5.3-3nl0iE8l74LLcBGaPKnj2w depends on profunctors-5.3 which failed
to install.

So... we wait.

Borrow good ideas from moniker

moniker is a binding combinators library for Rust that takes its inspiration from Unbound.

https://github.com/brendanzab/moniker

One interesting design decision was to split up Alpha into a pattern type class and a term type class.

The main change that we make is to have two separate traits (BoundTerm and BoundPattern) in place of Unbound's single Alpha type class. We've found that this better captures the semantics of the library, and greatly cuts down on the potential for accidental misuse.

It's worth exploring whether this makes sense for us, too.

unbind2Plus doesn't work with two different pattern types

using unbind2Plus on two different term types results in an error when printing the terms out, see an example below.

Using it on two terms of the same type doesn't cause errors, as far as I can see.

Example

{-# LANGUAGE DeriveAnyClass, DeriveGeneric #-}

import qualified Unbound.Generics.LocallyNameless as Unbound
import           GHC.Generics (Generic)
import           Control.DeepSeq (NFData, force)

type LName = Unbound.Name LTerm

data LTerm
  = -- | variables  `x`
    LVar LName
  | -- | abstraction  `\x. a`
    LLam (Unbound.Bind LName LTerm)
  | -- | application `a b`
    LApp LTerm LTerm
  deriving (Show, Generic, Unbound.Alpha, NFData)

type RName = Unbound.Name RTerm

data RTerm
  = -- | variables  `x`
    RVar RName
  | -- | abstraction  `\x. a`
    RLam (Unbound.Bind RName RTerm)
  | -- | application `a b`
    RApp RTerm RTerm
  deriving (Show, Generic, Unbound.Alpha, NFData)

lterm :: LTerm
lterm =
  let name = (Unbound.s2n "left")
      var = LVar name
  in LLam (Unbound.bind name var)

rterm :: RTerm
rterm =
  let name = (Unbound.s2n "right")
      var = RVar name
  in RLam (Unbound.bind name var)

test :: IO (LTerm, RTerm)
test = Unbound.runFreshMT $ do
  let (LLam lbind) = lterm
  let (RLam rbind) = rterm
  (lv, lb, rv, rb) <- Unbound.unbind2Plus lbind rbind
  return (lb, rb)

main :: IO ()
main = do
  (l,r) <- test
-- this doesn't cause errors
  let dl = force l
  let dr = force r
-- this does
  putStrLn $ show $ dl
  putStrLn $ show $ dr
-- that's it
  putStrLn "done"

Error

The error I'm getting while printing the right subterm is

LocallyNameless.open: inconsistent sorts
CallStack (from HasCallStack):
error, called at src/Unbound/Generics/LocallyNameless/Alpha.hs:717:20 in unbound-generics-0.4.2-B2j2SNQtM51IWCI7gQ83CW:Unbound.Generics.LocallyNameless.Alpha

Version

this is using GHC 9.02, but also works on at least 9.2.2
unbound-generics version is the current revision a2a5580

How to build the example

I have this example in a reproducible build. Here is a gist with all relevant files.

Substitution should continue traversing even when the variables do not match

Right now, when you try to apply a substitution, and the variables do not match, the traversal of the AST halts, even in cases when the substitution should continue.

For example:

type Var = Name Term
type MetaVar = Name Extract

data Term
data Term
    = Var Var
    | Hole MetaSubst MetaVar
    | Lam (Bind Var Term) 
    | App Term Term
    deriving (Show, Generic, Typeable)

newtype Extract = Extract { unExtract :: Term }
    deriving (Show, Generic, Typeable)

newtype MetaSubst = MetaSubst { metaSubst :: [(Var, Term)] }
    deriving (Show, Generic, Typeable)

instance Alpha Term
instance Alpha Extract
instance Alpha MetaSubst

instance Subst Term Term where
    isvar (Var x) = Just $ SubstName x
    isvar _ = Nothing
instance Subst Extract Term where
    isCoerceVar (Hole ms x) = Just $ SubstCoerce x (Just . applyMetaSubst ms x)
    isCoerceVar _ = Nothing

applyMetaSubst :: MetaSubst -> Extract -> Term
applyMetaSubst  e = substs ms $ unExtract e

instance Subst Term MetaSubst
instance Subst Extract MetaSubst

When performing the following substitution:

let mv = s2n "_"
     mv1 = s2n "_1"
     h = Hole (MetaSubst [(s2n "a", Hole (MetaSubst []) mv)]) mv1
in subst mv (Extract $ Var $ s2n "x") h

The result is Hole (MetaSubst [(a, Hole (MetaSubst []) _)]) _1 instead of the expected Hole (MetaSubst [(a, Var x)]) _1.

Unification function

Does unbound have a unification function? If not, does that seem like a good addition? To be precise, I'm looking for a function such that, given two terms t0, t1, finds a substitution s such that substs s t0 `aeq` t1, if it exists.

README example doesn't compile with recent GHC

I get this error message:

app/Main.hs:29:14: error:
    • No instance for (MonadFail Data.Functor.Identity.Identity)
        arising from a use of ‘fail’
    • In the first argument of ‘($)’, namely ‘fail’
      In the expression: fail $ "unbound variable " ++ show x
      In an equation for ‘eval’:
          eval (V x) = fail $ "unbound variable " ++ show x
   |
29 | eval (V x) = fail $ "unbound variable " ++ show x
   |              ^^^^

It seems the uses of fail should be changed to error.

Variable capture when substituting in `Rebind` patterns

Migrated from unbound issue tracker - issue 25

Should be able to detect & fail when capture occurs while substituting in patterns. For example,

subst (Rebind x y) (Var x)

will produce an invalid pattern. The variable x will still be a free variable, even though the pattern binds > variables of that name. This behavior should be caught and an appropriate error message given.

May also be worth having a Maybe version of subst.

Remove Show superclass on Alpha

This would be a breaking change for anything using show* but constrained only on Alpha. Bind's nthPatFind and namePatFind wouldn't be able to show the specific bind for errors anymore.

  • remove (Show a) => (Alpha.hs 160)
  • remove (Show t) => (Ignore.hs 35)
  • remove " ++ show b ++ " (Bind.hs 66/67)

(These are the changes that I could find.)

Help Wanted: Looking for co-maintainer

April 2023:
I (@lambdageek ) am not working on this package very actively anymore. I would love a co-maintainer who could invest some time in making sure that everything keeps working (and perhaps even address some of the long-standing issues).

What does Embed really do?

Hi, new user here: is there a functional difference between the following two representations of a type-annotated lambda?

data Exp  = ... | Lam  Ty (Bind (Name Exp)            Exp)
data Exp' = ... | Lam'    (Bind (Name Exp', Embed Ty) Exp')

Other than the fact that unbind and so on return different things, that is: are there differences in substitution, instantiation, and so on?

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.