Yvee1/hascard

Shuffling of cards

stemvork opened this issue · 10 comments

Dear Yvee1,

The recents list is highly useful. Are you considering to add keybinds/buttons to the CardSelector UI to open a deck from the recents list with options shuffle and or amount?

While typing this, I realise that chunking is also interesting. This would maybe look like -c 10 2 to split the list into 10 chunks and work on the second chunk.

Just realised that running hascard -s without file argument allows me to open any recent deck in shuffle mode. That could also become the workflow. I'd love to buy you a "coffee", where can we donate?

Kind regards,
stemvork

PS: I love the application. It was very easy to convert my learning material to the right format and immediately get practicing. Which then was way easier and more fun. I used the -s -a 5 setting to get started and then bumped the amount later. Finally, rehearse the list without options.

PPS: I am new to Haskell, have learned some language concepts and solved a few problems on HackerRank thusfar. Your application encourages me to learn further and the code is educational. Much respect.

I did some minimal editing while looking around and learning from the codebase. I changed maxRecents = 10 (UI/CardSelector.hs:161) and updated vLimit 6 to vLimit (maxRecents+1) (UI/CardSelector.hs:74).

I'm thinking of a next exercise to learn from/work on the codebase. If I were to implement the above feature, I have figured out that I need to add a case to (UI/CardSelector.hs:116) for the "S" key and execute a similar code block, ensuring the shuffle option.

Yvee1 commented

Hello again!

I'm glad you like it so much! Since I have vacation now, I haven't actually used the application much yet so I really appreciate your feedback. The shuffling and amount indeed also work without a file argument, perhaps I should make that clearer in the info and readme. It's maybe a bit confusing that there is a settings menu in the application itself and also some command line options. The idea behind it is that the settings menu in the application are the preferences that you'll likely not change often, and the CLI options, like the shuffle and amount, will vary a lot more often.

Having a shuffle option in the card selector is an interesting idea. It would indeed be nice to be able to disable/enable shuffling without exiting the application and running it again with another option. Using the s key for toggling the shuffle would work nicely I think, however I'm wondering if and how we would do the same with e.g. the amount option. Should a box appear in which you can fill in the amount of cards when you press the 'a' key?

The chunking option is also an excellent idea! Should a box to configure this also appear when pressing 'c' for example?

I made a page if you want to buy me a coffee: https://www.buymeacoffee.com/Yvee1, you're too kind.

I didn't think anybody would go through my code so I'm going to clean it up some more haha. But nice to hear you can understand how it works. Those changes to the codebase should work indeed, though for the 's' key it might be nicer to change the doShuffle in the GlobalState and add a widget indicating that shuffling is now turned on rather than directly running the deck. If you end up implementing the 's' key feature you could do a pull request and I'll check the code and merge it. Regarding the maxRecents, the vLimit change is a good one indeed, also I think a settting in the settings menu for changing the maxRecents value would be nice to have, what do you think?

You're welcome, and have a nice vacation. The general idea of the command line options and settings menu are clear and I agree completely. Smooth workflow. And the settings menu is a welcome improvement over the usual dotfile (or similar) to change the not-often-changed settings inside the application.

That indeed is exactly how I envision the upgrades to the card selector UI with regards to shuffling and the box that appears for the amount option.

My initial thought was to have the options per file. It could store (and show) the last used setting for each file. Since you might have a preference for going through the quiz-like decks front-to-back and the collection-like decks in a shuffled or chunked fashion. The list would carry indicators (per file) such as "S" or "Shuffling" and "5". Looking ahead, this might be complemented by a clearFlags method/keybind to clear the current flags on that file.

Hmmm.. but thinking this through, it gets very file-bound and might be overly complicated. I think you are right that shuffling, chunking and amounts should be global properties that can be enabled/disabled/changed. The application can give some overall visual feedback.

Hahaha, had some fun learning trying to animate the word Shuffling.

Yvee1 commented

Ah okay, I didn't think about having the properties linked to recently selected files. It does have advantages like you said, and it might not be such a bad idea. It depends on the expected usage. The default configuration should be what the user most likely wants. Currently the assumed usage is just going through all the cards linearly. But, I can imagine that someone who recently reviewed 5 cards from a shuffled vocabulary deck, does not want to go through all the cards linearly next time they select the deck.

I'm not sure what's best. I've thought about it a bit and think the options are:

  1. Link settings to all files via hidden files with the same name or a database or something
  2. Link settings to recently selected files, by storing the indicators like you said
  3. Store last-used settings globally in a config file, like the settings from the 'Settings' menu.
  4. Don't store anything and show all the cards linearly by default

We're now at 4. I think option 1 becomes too messy/complicated. Moving to option 2 or 3 is possible, since if you're not using the default options it might be a annoying to have to switch the settings each time. Option 4 is also not that bad since it doesn't take that much effort to do hascard -a 10 -s each time instead of just hascard.

Option 4 is the best if the person usually goes through all the cards linearly, because then that is the default. Option 3 is the best if the person has some other settings that they usually use, because that then becomes their 'default'. And option 2 for defaults per file, like you already discussed.

I find it hard to decide what's best. Especially considering the chunking and amount options. These most likely differ significantly between decks, so option 2 might be best

Yvee1 commented

Fun animation by the way haha!

In response to your think:

I also need to give it a think and maybe sketch. And as you say, it even depends on the user. The current default behaviour is a solid workflow. Even when implementing Option 2, this could be a feature that is disabled by default. Maybe something like this is enough:

  1. Launch hascard, optionally with args. These args are persistent.
  2. In the Select menu, have keybinds to adjust the options (s, c, a, etc.) and give visual feedback for enabling these flags. These are also persistent. That is, upon returning to the Select menu, these options are still on.
  3. From the Select menu, you can clear the current flags. This would either clear all flags, or the flags added since launch.
  4. As usual, upon exiting the application, any flags are forgotten.

In response to your hints:

Holy crap this feels awesome! After some struggle, I figured out how to do open the deck as shuffled with s. Thanks for the tips.

Have not yet figured out how to only update _doShuffle to True in the GlobalState variable. How do I go about updating the GlobalState? My naive attempts are something like:

V.EvKey (V.KChar 's') [] ->
  continue $ setShuffle s'

I have tried multiple things along the lines of [below], which runs infinitely.

setShuffle :: State -> State
setShuffle s = s { _gs = gs' }
 where gs' = gs' {_doShuffle = True }

At some point I wrote [below], which did not run infinitely, but I couldn't figure out how to update the State with this the GlobalState.

setShuffle :: GlobalState -> GlobalState
setShuffle gs = gs { _doShuffle = True }
Yvee1 commented

I'll also think a bit about it some more. In any case, keybindings for the Select menu is nice, and won't be a waste to implement either way.


Regarding your attempts:

You have probably noticed the State type is different per UI menu, but to convey information between the different UI menus I have a GlobalState type. This is a so-called record type, with _doShuffle indicating whether to shuffle. When clicking the s key we want to toggle the _doShuffle value in the GlobalState and also (eventually) change something in the State to indicate that a Widget has to be displayed as a visual indicator for the fact that shuffle has been turned on/off. Because we want to be able to access the global state in the CardSelector in the event handler, I put it inside the State in a field called _gs.

So we want to toggle _doShuffle in the GlobalState which is inside of the State. We can do this by adding a case like so:

V.EvKey (V.KChar 's') []  -> continue (s { _gs = _gs s { _doShuffle = not (_doShuffle (_gs s)) }})

(or something like this, I haven't tested it)

As you can see, working with nested records in plain Haskell is not so nice... Luckily there are libraries that make things easier, and a widely used technique are lenses from the lens library. This is what I use, and also why there are underscores before all the fields. With lens we can write the above mess like this:

V.EvKey (V.KChar 's') []  -> continue (s & gs.doShuffle %~ not)

This is like applying the not function on the field _doShuffle inside the field _gs. The %~ means applying a function, and the & is just for notational convenience.

Other lens things which you might find in the code are

 s & list .~ l'

this sets the _list field of s to the value l', so is equivalent to s { _list = l' }. Also viewing fields:

s ^. gs

which gets the value of the _gs field, so is the same as _gs s

Hope this helps a little! I'm happy to answer any other questions!

Yvee1 commented

I see I still haven't completely answered your question! So, to be clear, you could write:

setShuffle :: State -> State
setShuffle s = s & gs.doShuffle .~ True

Which is a correct way of want you wanted to do with

setShuffle :: State -> State
setShuffle s = s { _gs = gs' }
 where gs' = gs' {_doShuffle = True }

The reason your version runned infinitely is because of a typo / thought mistake. The correct version would be:

setShuffle :: State -> State
setShuffle s = s { _gs = gs' }
 where gs' = _gs s {_doShuffle = True }
V.EvKey (V.KChar 's') []  -> continue (s { _gs = _gs s { _doShuffle = not (_doShuffle (_gs s)) }})

Yeah, I tried something like that as well. How do I read the _gs = _gs s { .. part? I didn't have the s in front of the second brace. I understand why I felt close now that I see the corrected version, but can not follow the syntax of ... _gs s { .... After seeing this, my thoughts read it as "the _gs field of the s state".

Fired up the REPL and fooled around, and it's true! I learn quickly in your guidance :).

λ> data State = State { _list :: [String], _exception :: Maybe String, _gs :: Int } deriving Show
λ> s = State { _list = ["test", "this", "type"], _exception = Nothing, _gs = 3 }
λ> _list s
["test","this","type"]

I have started learning Haskell less than 2 weeks ago. Did come across lens, but definitely have zero practice with that. Thanks for the introduction to some of the operators.

It definitely helps a lot! Thanks for helping me along. This feature works like a charm now, and it's definitely useful.

Yvee1 commented

Great, you learn fast indeed. When you define a type and give names to the fields (i.e. record syntax) like done with State, then Haskell automatically makes 'getter' functions with the appropriate names. So in your example you can check in the ghci repl that :t _list gives

_list :: State -> [String]

You see it's just an ordinary function. So you're correct that what _gs s does is simply 'getting' the global state out of the state s. If you would omit the s it wouldn't work since _gs is a function and not a record; _gs s is a record. For a more elaborate explanation you might be interested in checking out the section about record syntax in 'learn you a haskell'.

If the feature works and you're happy with it you could submit a pull request if you want. In case you want more practice with Haskell you could try and implement other features we discussed in this issue like the visual indicator or other keybindings, and I would gladly merge them and help you figure them out. These things might be a little tougher though, so if you don't feel like doing it that's of course totally fine and I will work on implementing them in the following days.

Yvee1 commented

Chunking, changing the max recents in the settings menu, and the visual feedback and keybinding for shuffling you implemented are now in release 0.2.1.0. Thanks for your help and suggestions. I'll close this issue now, keybindings for the other CLI options could/have to still be implemented but best to open a new issue/pull request for that.