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

Plumb rich failure types #1116

Merged
merged 10 commits into from
Feb 22, 2023
Merged

Plumb rich failure types #1116

merged 10 commits into from
Feb 22, 2023

Conversation

kostmo
Copy link
Member

@kostmo kostmo commented Feb 18, 2023

closes #1115

This PR makes use of the rich types defined in #982 and converts many functions of Has (Throw Text) type to ExceptT.

It also logs a warning instead of crashing with terminal failure if "save" info cannot be loaded, which will be important for #974 where the save file schema changes.

Screenshot from 2023-02-18 13-22-16


data Asset = Achievement | Data | History | Save
data PathLoadFailure
= PathLoadFailure FilePath LoadingFailure
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you curry error constructor? 🙁

As far as I see, you do not use the PathLoadFailure for anything else. The SystemFailure in question is already named AssetNotLoaded so what do you get by introducing this extra step?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SystemFailure type has an Asset member, and it was unclear in certain low-level functions which asset is relevant. However, in the higher-level functions, the context becomes clear, and then I am able to use withExceptT to supplement the error with the Asset argument.

Copy link
Member

@xsebek xsebek Feb 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kostmo I only see the getData[Dir|File]Safe functions. Why do this hard, when you can simply pass them the AssetData type?

- left (AssetNotLoaded $ Data AppAsset)  <$> getDataDirSafe "."
+ getDataDirSafe AppAsset "."

This also makes more sense, because no Asset other than AssetData is in the data directory.

src/Swarm/Game/Entity.hs Outdated Show resolved Hide resolved
@xsebek
Copy link
Member

xsebek commented Feb 18, 2023

@kostmo thanks for working on this, once you are done I can rework logging to use these types. 👍

src/Swarm/Util.hs Outdated Show resolved Hide resolved
src/Swarm/Game/Recipe.hs Outdated Show resolved Hide resolved
@kostmo kostmo mentioned this pull request Feb 18, 2023
@kostmo kostmo marked this pull request as ready for review February 18, 2023 22:05
@kostmo kostmo requested a review from byorgey February 18, 2023 23:04
src/Swarm/Failure.hs Outdated Show resolved Hide resolved
src/Swarm/Game/Entity.hs Show resolved Hide resolved
src/Swarm/Game/Recipe.hs Show resolved Hide resolved
src/Swarm/TUI/Model/FailureRender.hs Outdated Show resolved Hide resolved
src/Swarm/Game/Scenario.hs Show resolved Hide resolved
src/Swarm/Game/ScenarioInfo.hs Show resolved Hide resolved
src/Swarm/Game/State.hs Outdated Show resolved Hide resolved
src/Swarm/Util/GameData.hs Outdated Show resolved Hide resolved
@@ -0,0 +1,37 @@
{-# LANGUAGE OverloadedStrings #-}

module Swarm.Failure.Render where
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kostmo @byorgey why is this split from the Failure module? It depends on nothing else.

AFAIK we do not have any other modules that have data declaration and basic functions separately.

Look at Exception.hs - it has data Exn and formatExn in the same module.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just commenting on a better place for the module to be, but I agree, it can/should probably just go in the Failure module itself.

Copy link
Member Author

@kostmo kostmo Feb 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case I had intended to use quoting functions from Swarm.Util in the rendering code. And up until 75669d7, Swarm.Util depended on types in the Failure module, causing a circular dependency. So I did the work of splitting the modules, but later decided I didn't need the quoting functions after all.

However, after doing that splitting work, I'm not particularly inclined to merge Swarm.Failure and Swarm.Failure.Render back into a single module again---IMO, there is no downside to the types and the rendering functions being in separate modules, even if there is no technical requirement to be so. I've found it to be a common step in the evolution of a module: auxiliary functions that had been self-contained with the data definitions eventually needing to pull in an external dependency, requiring the data to move to a separate "types" module to break a circular reference. It may very well become necessary again for Failure rendering at some point.

It fact, for data definitions that pervade the codebase, it is convenient for changes to that data's rendering functions not to cause the whole project to recompile. The only use case I've observed that demands co-locating data and their "methods" (to borrow OOP parlance) in the same module is smart constructors. My stance has been that the partitioning of Haskell code into modules is largely arbitrary, but in general I prefer smaller modules, and even splitting them pre-emptively.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kostmo that is inconsistent with the code base style though.

Rendering widgets - sure.

But pure functions that are critical to usability of the type (ParseError and prettyParseError) can absolutely go into the same module and they do everywhere else in Swarm.

Which language puts pure functions on data in different module? In C++ and Python, I have always seen the functions that are basically “methods” in the same module.

Regarding dependencies, your reasoning is backwards. The Util module should simply not import anything else. It has to be on the bottom of dependency graph. We should try to move stuff from it (as you did) instead of importing modules in it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finally from a practical standpoint this is premature and silly.

We have modules 2000+loc and argue here for splitting a 100loc module in a novel way, based on OOP best practise.

The future split could go another way though - split failures based on which part of game or UI produced it.
Whoever adds more code to failures in the future can decide which split fits better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a side note, the smart constructor use case is a good point, but it does not apply here.

We will be adding failure types and their pretty printing at the same time, not tweaking the pretty printing function. This is not about "rendering a widget" - I am not going to change the colour of exceptions tomorrow and recompile the whole code base because Text does not have a colour Attr that I could change.

What I am trying to point out is that failure data type and format are tightly linked, think of it like C++ e.what() method instead of failureWidgetWithAttr.

Copy link
Member

@xsebek xsebek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kostmo I like how you split the modules 👍 Please just move them as I commented.

Some of this will be easier once Cabal enforces it on split libraries, but doing that is a lot of not-very-fun work, so I would appreciate it if you made it easier for me.

I can not wait to have it done and get to more enjoyable things like rewriting the logging. 😅

Copy link
Member

@byorgey byorgey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good to me!

@kostmo
Copy link
Member Author

kostmo commented Feb 20, 2023

@xsebek, moved the new modules under Game. Good to go?

Copy link
Member

@xsebek xsebek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kostmo I think this is good enough to go. 👍

Please read my comment on PathLoadFailure though - it seems to me you are unnecessarily making the types harder to use.

I can not understand the urge to create separate "Render" modules, but I guess I can live with one extra folder and a file. I will just have to sort the folders and files alphabetically and not "folders first" so it is less annoying. 😒

@xsebek xsebek mentioned this pull request Feb 20, 2023
@kostmo kostmo added the merge me Trigger the merge process of the Pull request. label Feb 22, 2023
@mergify mergify bot merged commit 967bb6c into main Feb 22, 2023
@mergify mergify bot deleted the plumb-rich-failure-types branch February 22, 2023 08:12
mergify bot pushed a commit that referenced this pull request May 2, 2023
towards #866

NOTE: #1116 should be merged first so that the schema change of save files is less disruptive.

## Examples

Different criteria can have their own best score:
![image](https://user-images.githubusercontent.com/261693/219904496-fcd23ca0-b208-43e1-afc6-188acfe327cf.png)

All criteria share the same single best score:
![image](https://user-images.githubusercontent.com/261693/219904553-abe3011c-41b0-469c-b34d-95d84b91697a.png)

## Behavior notes

* As currently designed, the code size will only be scored if the the player has specified their code **before** the scenario begins.  Furthermore, any input into the REPL will invalidate code size scoring for the current game.
* Because of this, the only way to score code so far is with a command-line argument of `--run` or `--autoplay`.  However, #1010  shall implement code size scoring when a scenario is launched from the UI.
* In the "best scores" display, if multiple "best score" criteria were all from the same game, they will be consolidated.  If all criteria are for the same game, the criteria labels will be omitted.
* The code size metrics will not be displayed if a "best score" was not obtained via `--run`.

## Caveats

### `run` command

Currently, the code entailed in a `run "somescript.sw"` command is not transitively included, so using `run` make the code size score meaningless.

## Testing

### Unit tests

Run the subset of unit tests:

    scripts/run-tests.sh --test-arguments '--pattern "Tests.Precedence"'

### Manual integration tests

First, reset the score:

    rm -f ~/.local/share/swarm/saves/Tutorials_grab.yaml

Saving the following to `grab-soln.sw`:
```
move;
move; grab; turn back; move; turn back; move;
move; grab; turn back; move; turn back; move;
move; grab; turn back; move; turn back; move;
move; grab; turn back; move; turn back; move;
move; grab; turn back; move; turn back; move;
move; grab;
```
Run as follows:

    scripts/play.sh --scenario Tutorials/grab.yaml --run grab-soln.sw

This should establish a record for code size.

Then, play the Grab tutorial and immediately paste and run this in the REPL:

    move; move; grab; move; grab; move; grab; move; grab; move; grab; move; grab;

This solution is faster in terms of time, but should not displace the code-length record, since no code length should be recorded from a REPL solution.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merge me Trigger the merge process of the Pull request.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Adopt rich Failure types throughout asset loading code
3 participants