Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plans for a future rewrite #111

Open
langfield opened this issue Nov 26, 2022 · 17 comments
Open

Plans for a future rewrite #111

langfield opened this issue Nov 26, 2022 · 17 comments

Comments

@langfield
Copy link
Owner

langfield commented Nov 26, 2022

Considering a rewrite, possibly in another language.

As such, I'll be pausing development on the sql-parser branch temporarily, reserving the right to walk back on this decision, and starting on a new haskell branch. The structure of the repository will change dramatically. There will be a single (1) source file called Main.hs containing all type definitions, all ORM stuff, all helper functions, and all business logic. There will be a single (1) test file called Test.hs containing QuickCheck tests of everything in Main.hs. Both of these files will live at the top-level of the repository, and I see really no need for there to be any subdirectories at all, perhaps barring generated API documentation, if I can't figure out how to build that automatically.

In order to handle future distribution in the form of an addon, I will simply build binaries for all relevant architectures, and include all of them in the addon package. Barring this, people will have to build themselves with stack, but I will endeavor to ensure that is never necessary. There will be a microwrapper python program that only exists to detect the platform architecture and call the relevant Haskell executable.

It is very possible to build portable binaries for macOS, Linux, and Windows via stack in CI, and indeed there have been lengthy discussions on the topic.

The work on sql-parser will not have gone to waste, because we have proven that we don't need the anki python package for nearly as much as I thought. In particular, the clone and pull commands can absolutely be written using only sqlite-simple and a custom parser analogous to the one we wrote in Lark to parse the output of sqldiff. I think it may even be possible to do the entirety of the push operation without relying on calling functions from anki. We'll be reimplementing a tiny portion of the rust backend, but I believe it will be fairly simple.

Starting from scratch, I'll follow very closely the model given in Algebra-Driven Design by Sandy Maguire, and define the algebra entirely on paper before writing any code. I also plan to make heavy use of QuickSpec to check that things work out equationally as I expect.

I have not yet decided whether to either work entirely in a combination of IO and Either, to handle warnings and errors, or to simply try out Polysemy. Probably the former to start with, because it is simpler.

To be continued...

@langfield
Copy link
Owner Author

langfield commented Dec 3, 2022

A slight amendment to the plan described above: I will not be planning the type algebra out on paper before writing any code. This is because I found it impossible to specify the type signature of every helper function without writing implementations. It simply wasn't clear the return types I was picking were even possible.

I have not given up on working from the algebra backwards (or perhaps we should say forwards). However, the plan is now to give a minimal implementation, following one's nose, then generate a spec via QuickSpec, and then use this to do a rewrite, starting with the type algebra.

In the meantime, I have also found that the property-based testing ecosystem in Haskell is both deeper and wider than I initially imagined.

@langfield
Copy link
Owner Author

langfield commented Dec 3, 2022

A meta-review of the friends of Haskell's QuickCheck

Our purpose here is to be complete and concise. Below are lists of links to all tools for property-based testing and its applications in Haskell, ordered roughly from popular to obscure.

Property-based testing libraries

  • QuickCheck - generates random test cases, you must have an instance of Arbitrary to generate data using combinators like choose, which picks a random value within a range. Can optionally do shrinking by implementing shrink. Instances for most types in base.
  • Hedgehog - random, you must write generators manually instead of having them inferred from a typeclass instance like QuickCheck's Arbitrary. Shrinking is integrated.
  • SmallCheck - generates enumerative test cases, verifies properties for all test cases up to some depth, must write Serial instance, serial maps depth to a list of values, easier to write than arbitrary. Can use existential operators easily. Exponential explosion of time complexity with depth.
  • Validity - random, you get free instances of the generator-inferring typeclass for any instances of Generic, can define data invariants in code, nice property combinators like symmetry.
  • LeanCheck - enumerative, properties are defined as functions that should return True for all choices of argument values, similarly size-based like Feat, so no explosion of time complexity. Argument types must be Listable, which is like SmallCheck's Serial, easier to write than QuickCheck's Arbitrary. Can infer these instances for arguments with no data invariants. Very well-maintained.
  • SmartCheck - random, like QuickCheck except shrinking is deterministic and preserves the constructors for counterexamples, and it generalizes counterexamples, e.g. it finds a counterexample [2, 2] and can compute the general form x:x:ys.
  • Feat - enumerative, size-based rather than depth-based, so time complexity does not explode as bad as SmallCheck. Must write a function from natural numbers (sizes) to values. Great for ADTs, where it may count number of constructors called.
  • Lazy SmallCheck - enumerative, like SmallCheck, but uses laziness and partially defined values to guide testing, greatly reducing the number of cases that need to be tested. Search space pruning does not work for strict properties, those that need to fully evaluate all arguments to return True or False.
  • GenCheck - random or enumerative, similar goals to Feat.
  • Irulan - random or enumerative, tests implicit properties, like the absence of assertion errors, incomplete pattern matches and other types of exceptions. Does so automatically, you need not write any typeclass instances. Can also do property testing. Generates data from the type of constructors. Unmaintained.

Equational specification generators

  • QuickSpec - you give it functions to describe, background functions it is allowed to use, and it tests your functions using QuickCheck and prints out equations which seem to hold.
  • EasySpec - like QuickSpec, but it can figure out what background functions to use automatically.
  • Speculate - like QuickSpec, but can generate inequalities and conditional laws.

Property refining libraries

  • MuCheck - you give it properties you think hold, and the implementation, and it mutates the source code, killing mutants that fail the properties. Any surviving mutants might indicate a missing property about the program under test.
  • FitSpec - a modern, beefed-up, maintained version of MuCheck. Another incredible library from the Matela-verse.

Function synthesis

  • Conjure - Probably the wildest library in the Matela-verse. Infers the implementation of entire functions from their type signatures and partial definitions. You give it a few elements of the domain and their images, and it conjures up something that works. Haven't seen anything like this outside of Idris's automatic hole-filling (which probably is a bit more powerful, but that is because Idris has a much stronger type system). Uses LeanCheck and Speculate behind-the-scenes.

Companion libraries

  • generic-random - automatically derives QuickCheck Arbitrary instances.

General recommendations

There may not be many reasons to prefer random implementations over enumerative. With enumerative frameworks, the problem of shrinking just doesn't exist, your tests are deterministic, custom instances are easier to write, and in all likelihood, you probably find bugs faster. I'd tentatively say to avoid Hedgehog, as it appears to have all the worst design decisions rolled into one package (random, no free generators/instances, integrated shrinking), despite its popularity/SEO ranking.

You can't really practice Elliott-Maguire denotational design without a spec generator. Best practice is probably to live in the Matela-verse and use LeanCheck, Speculate, and FitSpec, or, if you want to stick to more tried-and-true stuff, go with QuickCheck or SmallCheck, in combination with QuickSpec.

@langfield
Copy link
Owner Author

I have been using gitlib, but it has occurred to me that doing this is making the problem more complex with no benefit. While gitlib is high-level, it is not high-level in the same way that GitPython or the git CLI is, and this is what we need. One would not need to think about blobs at all if doing these operations manually, and so there is no need for me to have to think about them while writing this code, which should just be an automation of the manual procedure.

As such, I'm switching to libgit (aren't these all named rather too similarly?) which is basically just a wrapper around the CLI.

@dobefore
Copy link

It sounds awesome!
Why not just use Rust instead, you seem trying to implement a small rust backend.

@langfield
Copy link
Owner Author

@dobefore Thanks very much for your comment. I am implementing a very small subset of the rust backend. The rust backend of Anki, if you've read much of it, is much too complex. In particular, I need not deal with any information related to reviews. It is a dreadful experience trying to trace a function call through that codebase to figure out what it's actually doing.

More generally, Rust makes code significantly safer, easier to read, and smaller than it would be if written in C. It does this via some nice abstractions and features/constructs that C simply does not have. Similarly, Haskell makes code significantly safer, easier to read, and smaller than it would be if written in Rust. And this is for the same reason, it has abstractions and language features that Rust does not have.

In particular, I would like to see someone try and write something like Speculate in Rust. I suspect it would be impossible, since equational reasoning about functions cannot really be done without referential transparency.

@langfield
Copy link
Owner Author

langfield commented Dec 25, 2022

Effect systems

There is brief mention of effect systems above. For a while now, the main contenders for the Haskell ecosystem have been freer-simple, fused-effects, and polysemy. Some sentiments expressed on the internet are that freer-simple is too slow, fused-effects has too much boilerplate, and polysemy is probably the best option, but still a bit slow. Moreover, using any of them without knowledge of free monads, coming from mtl, well, it's a bit confusing. There is also eff, which has been abandoned, but promised to be like freer-simple, only very fast.

However, there are two lesser-known options taking a completely different approach that seems awfully promising. Both effectful and cleff distinguish themselves by sidestepping the use of free monads completely in their implementation. The Eff monad in either case is essentially equivalent to ReaderT IO. There are both extremely fast, and are comparable with polysemy when it comes to user-friendliness and boilerplate. While effectful appears to be better supported and slightly more used than cleff, cleff has the distinction of having a remarkably simple interface and extremely easy-to-read documentation. Furthermore, the authors have collaborated on design decisions, which has likely strengthened both implementations. It is also notable that both cleff and effectful manage to outperform mtl! (according to effectful's microbenchmarks)

Without having used either in a major project, I cannot make any informed recommendations. However, if I move this tool to an effect system, cleff is probably going to be the first thing I try.

@langfield
Copy link
Owner Author

langfield commented Jan 1, 2023

Micro task queues

This will be an experiment testing a goofy little trick to keep me on-task. The basic idea is that I will break down each hour of development into roughly 5 minute tasks. Hopefully this will give me a better idea of the superstructure of the stuff I need to do next, and also make things go faster. Perhaps I can also log the time I started and the time I stopped.

  • Write Makefile
  • Try compiling the whole thing
  • Look up ways to get around undefined reference error

@langfield
Copy link
Owner Author

Linking C libraries in Haskell

The following resources were useful:

@DonCiervo
Copy link

Hi there, just stumbled across this project and will give it a try over the weekend - seems awesome!

What does the Haskell rewrite mean for the project, the current issues raised and ways to contribute? I'd love to contribute to the project in a way, but I'm not (yet) comfortable in Haskell.

@langfield
Copy link
Owner Author

langfield commented Jan 3, 2023

@DonCiervo I'm looking forward to hearing how it works for you! Let me know if there's issues you run into that I might be able to fix. (Feel free to open more issues!)

The Haskell rewrite means that eventually, the python version will be deprecated. At that point, the command-line version of the tool will be distributed either as signed binaries, right here on Github, or alternatively, I may consider distributing the binaries via pip if it seems that's easier for people.

As for the current issues raised, they will likely not be fixed in the python codebase in the meantime. A lot of the open issues are simply features I thought might be necessary or improvements to the development workflow. There is only one real bug that could impact users, which is #109, and it only matters if you have notes whose cards are split between decks. This is at least somewhat uncommon, and only rears its head if you make edits to those notes on the markdown side.

I've found solutions to nearly all of these things on the Haskell side. The biggest problem I was running into was that it was simply too onerous and too slow to perform effective property-based testing on the python codebase. Hypothesis is the best thing folks have in the python ecosystem, but it is quite unpleasant to use, in my opinion.

Ways to contribute

I'm extremely happy to hear that you're interested in contributing, and I will bend-over backwards to make it as easy and enjoyable as possible for you if you are committed to helping out. There are a number of ways you can do this without even writing any code. Here's a list for you to consider:

  • Just use the tool - Getting an idea for how users will interact with this thing is very important to me. Feel free to send as much feedback as you like in issues. Complaints are more useful than compliments. The documentation is thorough but sorely outdated. Complain about parts you don't like or stuff that needs to be updated, and I will fix it.
  • Hang out in the issues - If you'd like to participate in high-level discussions of the design decisions happening during the rewrite, this would also be very helpful. There's lots of guesswork involved in deciding how things should function and what the end-users will need. A second opinion is always welcome.
  • Bring in more users - Convincing more people in the Anki community to try this out would be helpful for obvious reasons. More feedback will make it better.
  • Support deck maintainers - There are already a few maintainers on Github who I've convinced to use Ki. They may be on older versions, and they may run into trouble as I put new versions out, as we're still in alpha. Simply checking in with these guys to see how they're doing would be useful.
  • Contribute to the codebase - This would be the most useful sort of help, obviously. But I would understand if you were unwilling to pick up a whole new language. However, I can tell you that coming from the python ecosystem, programming in Haskell feels like someone took a list of everything that was wrong or bad with python and fixed all of them, then added a bunch of extra improvements I'd have never even thought of on top of that. See Co-Star's explanation of why they use it.

On contributing code

There are a couple ways you could do this.

As an individual. If you're comfortable, I could simply pick some very simple tasks that would get you warmed-up to the language and the codebase (which is still miniscule), and you could just run with it.

As a side-along dev. If you like pair programming and have some free time for it, I could also spend some time each week bringing you up to speed on some sort of call using a collaborative environment or screenshare. It would be easier to get a handle on the repository and the implicit knowledge that goes along with the code in this way, for obvious reasons.

Whatever you choose to do, I encourage you not to take on too much too fast. I've been working on this for about a year now, and it would be much more useful to have a very small amount of someone's time over a long timescale rather than a couple weeks of intense attention.

@DonCiervo
Copy link

DonCiervo commented Jan 4, 2023

@langfield Cheers, appreciate the extensive answer. Maybe you should consider copying this information to a separate markdown file entirely so newcomers can get a quick overview. Personally I am also very partial to pinned issues for any "Wanna help? Here's how" type stuff.

Regarding the move away from Python, what is the long-term goal for ki ? Granted I haven't installed it yet, but it seems the basic functionality is there, judging from the documentation here and the fact it's being actively used. Surely the best next step to gain more users would be to package this into an add-on that lets people connect the decks of their choice to an upstream repo from within Anki.

Now, I am aware this is by no means as easy as I'm making it out to be - but neither is a rewrite in Haskell. I get that Haskell is one of the more beautiful languages to use, but if maintainability and stability is the goal, then the Haskell environment doesn't have the best reputation either, if I'm being honest.

@langfield
Copy link
Owner Author

langfield commented Jan 4, 2023

Long-term goals

An addon is planned, but it's rather low priority for me. This tool is first-and-foremost for my own use, and I have no desire for a GUI. It is also quite a tricky problem to figure out how to allow users unfamiliar with git or the command-line to resolve merge conflicts intelligently.

I am more interested in rolling things out to deck maintainers first, and the next big user-facing feature I have planned is rendering collections via Jekyll in a Github actions workflow. See #41. Deck maintainers are a bit more tolerant of bugs and sharp edges, and make excellent alpha testers. They also better appreciate the usefulness of git.

Language choice

As I mentioned earlier, the problem with Python was that I was unable to write the sort of robust test suite that is possible in some other languages. I was finding critical bugs too often, and the code was simply unsafe.

Re: maintainability and stability: package distribution via Stack/Stackage is extraordinarily stable. The resolver system means that ordinarily, the developer never even has to think about compatibility of dependencies. All dependencies packaged for a given resolver are compatible by definition. It is very common, more the rule than the exception, for projects to go unmaintained for 5-10 years simply because there are no changes required to the code to keep it working for end-users.

Nowhere is this more obvious than from the commit count. In the Python ecosystem, one great heuristic I used for determining if a project was stable or not was the number of commits in the Github repository. All else equal, it is usually the case that more commits indicates more active maintainers who fix bugs and keep things working as the language and dependencies change. They avoid bitrot, in other words.

It was quite a surprise to learn that in the Haskell ecosystem, this heuristic works poorly. Relatively "famous" libraries often have under 100 commits, because that's simply all that's needed to get the code into a stable and complete state. Take QuickCheck, for example. This is a library so famous that it has its own Wikipedia page, and is where the idea for property-based testing got its first implementation. It has been ported to over a dozen other languages, and the Python version, hypothesis, is a classic example of a large, mature python project. It has over 12000 commits and hundreds of contributors, and was started roughly 7 years ago.

QuickCheck, on the other hand, has only ~1100 commits, and was started in 1999, and has a Git history that goes back 16 years. It has taken an order of magnitude less commits to keep it stable and working, and it works dramatically better than its Python counterpoint.

It is also notable that Haskell is extraordinarily fast compared to Python, as it compiles down to native machine code, and the compiler is state-of-the-art. It is able to hold its own against systems languages like Rust, Go, C in benchmarks. You may note that tasks that take Haskell roughly 10s require almost 10mins in Python. That's a ludicrous slowdown. You get programs that run massively slower and are also roughly an order of magnitude more likely to break at runtime.

If you trawl through the Anki forums, you will notice that one of the first complaints I got about Ki was that it was simply too slow. I did a lot of work on optimizing the Python version, and got quite far, but it would all be unnecessary in Haskell. This problem is really not computationally intensive enough to require heavy optimization in a compiled language in order for the user experience to be quick and snappy.

Additionally, there are things you can do in Haskell that are just impossible in Python. The type system is so strong that you can write something like Conjure and have it actually work. This is a library for function synthesis. You write a few example inputs and outputs of a function, and the library uses the type system to literally write the implementation for you.

@DonCiervo
Copy link

Having an interactive github pages front-end would be very close to appeal to even a more basic user, so that would make a great feature, methinks. I should probably get a bit more familiar with the program as it is and look into the issues one by one before I ask any more questions that have already been answered somewhere.

The problem of a slowdown is obviously the big letdown in Python, and also the reason why the Anki backend has moved away from it. I was curious why you were deadset on using Haskell instead of Rust, for instance. But with the direction you're taking the project in, it only makes sense.

@langfield
Copy link
Owner Author

langfield commented Jan 4, 2023

look into the issues one by one before I ask any more questions that have already been answered somewhere.

Don't worry about this too much, I'm happy to answer any questions you might have. Discussing existing issues and problems will probably serve to clarify them anyway.

The problem of a slowdown is obviously the big letdown in Python, and also the reason why the Anki backend has moved away from it. I was curious why you were deadset on using Haskell instead of Rust, for instance. But with the direction you're taking the project in, it only makes sense.

Yes after writing collection ops in pure python, I have a greater appreciation for just how fast the rust backend is.

However, I'm not sure I'd say the slowdown is the biggest letdown. Correctness guarantees, IMO, are more important. If haskell were slow and python were fast, I'd still strongly consider switching.

@langfield
Copy link
Owner Author

langfield commented Jan 21, 2023

The first pseudo-correct output from clone!

(base) user@computer:~/proving-grounds$ cd ~/pkgs/ki && stack install && cd - && rm -rf dd && Ki-exe multifield/collection.anki2 dd && tree -a -I .git --filelimit 100 dd
Copying from /home/user/pkgs/ki/.stack-work/install/x86_64-linux/e79dab73246224887016704ede3d4c53affc0117456ea68c975b50a5c094bcee/8.10.7/bin/Ki-exe to /home/user/.local/bin/Ki-exe

Copied executables to /home/user/.local/bin:
- Ki-exe
/home/user/proving-grounds
Cloning media from Anki media directory '/home/user/proving-grounds/multifield/collection.media/'...
parts: ["Default"]
Committing contents to repository...
Done!
dd
├── Default
│   └── abcd
├── .gitignore
├── .gitmodules
├── .ki
│   ├── config
│   └── hashes
├── _media
└── _models
    ├── Mid 1673577708734.yaml
    ├── Mid 1673577708735.yaml
    ├── Mid 1673577708736.yaml
    ├── Mid 1673577708737.yaml
    ├── Mid 1673577708738.yaml
    ├── Mid 1673577710620.yaml
    ├── Mid 1673577743038.yaml
    └── Mid 1673577758568.yaml

4 directories, 13 files

The things that are not yet quite right:

  • Missing .md suffix on note file
  • Only sort field should be included in the note filename, assuming it is nonempty
  • The model filenames should be the model names
  • The model file contents are slightly wrong and incomplete

@langfield
Copy link
Owner Author

Even closer-to-correct clone output!

(base) user@computer:~/proving-grounds$ tree -I .git -a --filelimit 25 bb
bb
├── [1] Main Course
│   ├── [a] Option 1: Parisian French Audio
│   │   ├── I) French to English (Start here)
│   │   │   └── 5000 Most Common French Words  [500 entries exceeds filelimit, not opening dir]
│   │   └── II) English to French
│   │       └── 5000 Most Common French Words  [500 entries exceeds filelimit, not opening dir]
│   └── [b] Option 2: Canadian French Audio
│       ├── I) French to English
│       │   └── 5000 Most Common French Words  [500 entries exceeds filelimit, not opening dir]
│       └── II) English to French
│           └── 5000 Most Common French Words  [500 entries exceeds filelimit, not opening dir]
├── [A. 1] Irregular Verbs Training
│   ├── [a] Option 1: Most Frequent Conjs. Come First
│   │   └── 5000 Most Common French Words  [47 entries exceeds filelimit, not opening dir]
│   ├── [a] Option 2: One Verb at a Time
│   │   └── 5000 Most Common French Words  [47 entries exceeds filelimit, not opening dir]
│   ├── [b] Past Participle
│   │   └── 5000 Most Common French Words  [46 entries exceeds filelimit, not opening dir]
│   ├── [c] Present Participle
│   │   └──  Gerund
│   │       └── 5000 Most Common French Words  [47 entries exceeds filelimit, not opening dir]
│   ├── [d] Imperative
│   │   └── 5000 Most Common French Words  [47 entries exceeds filelimit, not opening dir]
│   └── [e] Literary
│       └──  Poetic Verb Tenses
│           ├── Past Historic
│           │   └── 5000 Most Common French Words  [47 entries exceeds filelimit, not opening dir]
│           └── Subjunctive Imperfect
│               └── 5000 Most Common French Words  [47 entries exceeds filelimit, not opening dir]
├── [A. 2] The Study of Sounds (Phonology)
│   ├── I) Basic IPA & Phonetics
│   │   ├── [1] IPA Letters
│   │   │   └── 5000 Most Common French Words
│   │   │       ├── baie.md
│   │   │       ├── boue.md
│   │   │       ├── chou.md
│   │   │       ├── clé.md
│   │   │       ├── cou.md
│   │   │       ├── doux.md
│   │   │       ├── fou.md
│   │   │       ├── gnouf.md
│   │   │       ├── goût.md
│   │   │       ├── jeune.md
│   │   │       ├── jeûne.md
│   │   │       ├── joue.md
│   │   │       ├── là.md
│   │   │       ├── loup.md
│   │   │       ├── mou.md
│   │   │       ├── nous.md
│   │   │       ├── peau.md
│   │   │       ├── pou.md
│   │   │       ├── roue.md
│   │   │       ├── si.md
│   │   │       ├── sort.md
│   │   │       ├── sous.md
│   │   │       ├── tout.md
│   │   │       ├── vous.md
│   │   │       └── zou.md
│   │   ├── [2a] Comparing French & English phonemes
│   │   │   └── 5000 Most Common French Words
│   │   │       ├── an-unreleased-stop-is-a-hard-consonant-that-does-not-relea.md
│   │   │       ├── compared-to-the-english-a-the-french-a-is-spoke.md
│   │   │       ├── compared-to-the-english-e-the-french-e-is-spoke.md
│   │   │       ├── compared-to-the-english-ɛ-the-french-ɛ-is-spoke.md
│   │   │       ├── compared-to-the-english-i-the-french-i-is-spoke.md
│   │   │       ├── compared-to-the-english-o-the-french-o-is-spoke.md
│   │   │       ├── compared-to-the-english-ɔ-the-french-ɔ-is-spoke.md
│   │   │       ├── compared-to-the-english-u-the-french-u-is-spoke.md
│   │   │       ├── how-is-the-french-b-d-g-different-from-english-b-d-g.md
│   │   │       ├── how-is-the-french-p-t-k-different-from-english-p-t-k.md
│   │   │       ├── place-of-articulation-is-where-a-consonant-is-formed-in-the.md
│   │   │       └── to-illustrate-the-difference-between-aspirated-and-unaspirat.md
│   │   ├── [2b] An illustration of French & English vowels
│   │   │   └── 5000 Most Common French Words
│   │   │       └── vowels-uncompressed02png.md
│   │   ├── [2c] Audio comparison of English & French Vowels
│   │   │   └── 5000 Most Common French Words
│   │   │       ├── comparing-french-n-and-english-nfrench-vowels-in-sequenc.md
│   │   │       ├── comparing-french-s-and-english-sin-french-s-the-tip-o.md
│   │   │       ├── comparing-french-z-and-english-zin-french-z-the-tip-o.md
│   │   │       ├── french-b-p-and-english-b-p-notice-how-french-release.md
│   │   │       ├── french-t-d-and-english-t-dnotice-how-french-releases.md
│   │   │       └── using-real-and-made-up-words-this-card-will-compare-french.md
│   │   └── [3] Notes on the IPA used in this deck
│   │       └── 5000 Most Common French Words
│   │           └── about-the-ipa-in-this-deck-let-a-be-any-vowel-let-l-be.md
│   ├── III) Aspirated h
│   │   ├── [1] Intro
│   │   │   └── 5000 Most Common French Words
│   │   │       └── this-deck-contains-words-that-are-aspirated-h-and-words-t.md
│   │   └── [2] Practice
│   │       └── 5000 Most Common French Words  [59 entries exceeds filelimit, not opening dir]
│   ├── II) Words With Irregular Pronunciation
│   │   ├── [1] Intro
│   │   │   └── 5000 Most Common French Words
│   │   │       └── in-a-way-or-another-the-words-of-this-deck-have-irregular-p.md
│   │   ├── [2a] Sorted by frequency
│   │   │   └── 5000 Most Common French Words  [96 entries exceeds filelimit, not opening dir]
│   │   └── [2b] Sorted by group
│   │       └── 5000 Most Common French Words  [96 entries exceeds filelimit, not opening dir]
│   ├── IV) Museum of Sounds
│   │   ├── [1] Intro
│   │   │   └── 5000 Most Common French Words
│   │   │       └── the-purpose-of-this-deck-is-to-showcase-the-sounds-of-the-fr.md
│   │   └── [2] Showcase (Formal Accent)
│   │       ├── a. Parisian French
│   │       │   └── 5000 Most Common French Words  [39 entries exceeds filelimit, not opening dir]
│   │       └── b. Canadian French
│   │           └── 5000 Most Common French Words  [39 entries exceeds filelimit, not opening dir]
│   └── V) The Skill of Listening
│       ├── [1] Intro
│       │   └── 5000 Most Common French Words
│       │       └── this-deck-has-the-purpose-of-training-your-listeningthere-a.md
│       ├── [2] Formal Accent
│       │   ├── a. Parisian
│       │   │   └── 5000 Most Common French Words  [1000 entries exceeds filelimit, not opening dir]
│       │   └── b. Canadian
│       │       └── 5000 Most Common French Words  [1000 entries exceeds filelimit, not opening dir]
│       └── [3] Informal Accent
│           └── a. Mixed Accents (98% Parisian)
│               ├── ii. Longer Sentences
│               │   └── 5000 Most Common French Words  [1323 entries exceeds filelimit, not opening dir]
│               └── i. Short Sentences
│                   └── 5000 Most Common French Words  [1492 entries exceeds filelimit, not opening dir]
├── [A. 3] Read & Speak Training (Where the Real Learning Happens)
│   ├── I) Basic
│   │   ├── [0] Note about this deck
│   │   │   └── 5000 Most Common French Words
│   │   │       └── 1-all-sentences-in-this-deck-are-present-in-the-main-deck.md
│   │   ├── [1] Read
│   │   │   └── 5000 Most Common French Words  [2305 entries exceeds filelimit, not opening dir]
│   │   └── [2] Speak
│   │       ├── a. Short sentences
│   │       │   └── 5000 Most Common French Words  [1000 entries exceeds filelimit, not opening dir]
│   │       ├── b. Medium-sized sentences
│   │       │   └── 5000 Most Common French Words  [676 entries exceeds filelimit, not opening dir]
│   │       └── c. Long sentences
│   │           └── 5000 Most Common French Words  [629 entries exceeds filelimit, not opening dir]
│   └── II) Intermediate
│       ├── [0] Note about this deck
│       │   └── 5000 Most Common French Words
│       │       └── 1-i-took-the-wiktionary-articles-for-each-word-in-the-5000.md
│       ├── [1] Read
│       │   └── 5000 Most Common French Words  [1154 entries exceeds filelimit, not opening dir]
│       └── [2] Speak
│           └── 5000 Most Common French Words  [1154 entries exceeds filelimit, not opening dir]
├── [B] Download this deck in e-book form
│   └── 5000 Most Common French Words
│       └── the-main-course-of-this-deck-5000-most-common-french-words.md
├── .gitignore
├── .gitmodules
├── .ki
│   ├── config
│   └── hashes
├── _media  [6755 entries exceeds filelimit, not opening dir]
└── _models
    ├── 01 BASIC.yaml
    ├── 5000 French Words 2.0 (E to F) C.yaml
    ├── 5000 French Words 2.0 (E to F).yaml
    ├── 5000 French Words 2.0 (F to E) C.yaml
    ├── 5000 French Words 2.0 (F to E).yaml
    ├── Basic (and reversed card).yaml
    ├── Basic (optional reversed card).yaml
    ├── Basic (type in the answer).yaml
    ├── Basic.yaml
    ├── Cloze (overlapping).yaml
    ├── Cloze.yaml
    ├── French aspirated h.yaml
    ├── French Ear Training.yaml
    ├── French IPA deck.yaml
    ├── French irregular pronunciation.yaml
    ├── French phonology.yaml
    ├── French sentences Read Training.yaml
    ├── French sentences Speak Training.yaml
    ├── French Verbs.yaml
    ├── French vowels comparison.yaml
    └── Intro card.yaml

100 directories, 77 files

@langfield
Copy link
Owner Author

See #39 for schemas.

@langfield langfield changed the title Rewrite entire program in Haskell Rewrite entire program in Haskell? Mar 21, 2024
@langfield langfield changed the title Rewrite entire program in Haskell? Plans for a future rewrite Aug 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants