├── .assets
└── img
│ └── logo.png
├── .coderabbit.yaml
├── .envrc
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .hlint.yaml
├── .vscode
├── extensions.json
├── settings.json
└── tasks.json
├── LICENSE
├── README.md
├── cabal.project
├── cli
├── LICENSE
├── app
│ └── Main.hs
├── default.nix
├── nhcli.cabal
├── src
│ ├── Neo.hs
│ └── Neo
│ │ ├── Build.hs
│ │ ├── Build
│ │ └── Templates
│ │ │ ├── AppMain.hs
│ │ │ ├── Cabal.hs
│ │ │ └── Nix.hs
│ │ ├── Core.hs
│ │ ├── Core
│ │ └── ProjectConfiguration.hs
│ │ ├── New.hs
│ │ ├── New
│ │ └── Templates
│ │ │ ├── MainModule.hs
│ │ │ └── NeoJson.hs
│ │ └── Run.hs
└── test
│ └── Main.hs
├── context
├── code-style.md
├── collections.md
└── documentation.md
├── core
├── LICENSE
├── concurrency
│ ├── AsyncIO.hs
│ ├── Channel.hs
│ └── ConcurrentVar.hs
├── core
│ ├── Accumulator.hs
│ ├── Array.hs
│ ├── Basics.hs
│ ├── Bytes.hs
│ ├── Char.hs
│ ├── Console.hs
│ ├── Core.hs
│ ├── Function.hs
│ ├── IO.hs
│ ├── Int.hs
│ ├── LinkedList.hs
│ ├── Map.hs
│ ├── Maybe.hs
│ ├── Record.hs
│ ├── Result.hs
│ ├── Task.hs
│ ├── Text.hs
│ ├── Tuple.hs
│ ├── Unit.hs
│ ├── Unknown.hs
│ ├── Uuid.hs
│ ├── Var.hs
│ └── Version.hs
├── default.nix
├── http
│ ├── Http.hs
│ └── Http
│ │ └── Client.hs
├── json
│ └── Json.hs
├── nhcore.cabal
├── options-parser
│ └── Command.hs
├── service
│ ├── Action.hs
│ ├── Service
│ │ ├── Event.hs
│ │ └── EventStore.hs
│ └── Trigger.hs
├── system
│ ├── Directory.hs
│ ├── File.hs
│ ├── Path.hs
│ ├── Subprocess.hs
│ └── Time.hs
├── test
│ └── Main.hs
├── toml
│ └── Toml.hs
└── traits
│ ├── Appendable.hs
│ ├── Applicable.hs
│ ├── Combinable.hs
│ ├── Default.hs
│ ├── Mappable.hs
│ ├── Thenable.hs
│ └── ToText.hs
├── default.nix
├── fourmolu.yaml
├── nix
└── nixpkgs.nix
├── project.yaml
├── sandbox
├── neo.json
└── src
│ └── Sandbox.hs
├── scripts
├── install.sh
└── run-doctest
└── shell.nix
/.assets/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neohaskell/NeoHaskell/c0654636c1409fb8b4d1dc4736a9877cab25b1c1/.assets/img/logo.png
--------------------------------------------------------------------------------
/.coderabbit.yaml:
--------------------------------------------------------------------------------
1 | language: en-US
2 | tone_instructions: >-
3 | You must use a respectful, but commanding tone, as if you were impersonating an AI deity.
4 | early_access: true
5 | enable_free_tier: true
6 | reviews:
7 | profile: assertive
8 | request_changes_workflow: true
9 | high_level_summary: true
10 | high_level_summary_placeholder: "@coderabbitai summary"
11 | auto_title_placeholder: "@coderabbitai"
12 | review_status: true
13 | poem: true
14 | collapse_walkthrough: false
15 | sequence_diagrams: true
16 | path_filters: []
17 | path_instructions:
18 | - path: "*.hs"
19 | instructions: |
20 | Remember that this is a NeoHaskell file. NeoHaskell is a
21 | Haskell dialect that is inspired by Elm, therefore the
22 | Elm style and conventions should be followed. Also,
23 | Elm core libs are available, and the Haskell Prelude is
24 | ignored, as the NoImplicitPrelude extension is enabled.
25 | abort_on_close: true
26 | auto_review:
27 | enabled: true
28 | auto_incremental_review: true
29 | ignore_title_keywords: []
30 | labels: []
31 | drafts: false
32 | base_branches: []
33 | tools:
34 | shellcheck:
35 | enabled: true
36 | ruff:
37 | enabled: false
38 | markdownlint:
39 | enabled: true
40 | github-checks:
41 | enabled: true
42 | timeout_ms: 90000
43 | languagetool:
44 | enabled: true
45 | enabled_only: false
46 | level: default
47 | enabled_rules: []
48 | disabled_rules:
49 | - EN_UNPAIRED_BRACKETS
50 | enabled_categories: []
51 | disabled_categories:
52 | - TYPOS
53 | - TYPOGRAPHY
54 | - CASING
55 | biome:
56 | enabled: true
57 | hadolint:
58 | enabled: true
59 | swiftlint:
60 | enabled: true
61 | phpstan:
62 | enabled: true
63 | level: default
64 | golangci-lint:
65 | enabled: true
66 | yamllint:
67 | enabled: true
68 | gitleaks:
69 | enabled: true
70 | checkov:
71 | enabled: true
72 | detekt:
73 | enabled: true
74 | eslint:
75 | enabled: true
76 | ast-grep:
77 | packages: []
78 | rule_dirs: []
79 | util_dirs: []
80 | essential_rules: true
81 | chat:
82 | auto_reply: true
83 | knowledge_base:
84 | opt_out: false
85 | learnings:
86 | scope: global
87 | issues:
88 | scope: global
89 | jira:
90 | project_keys: []
91 | linear:
92 | team_keys: []
93 |
--------------------------------------------------------------------------------
/.envrc:
--------------------------------------------------------------------------------
1 | use nix
2 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: "Test"
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | push:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | tests:
13 | strategy:
14 | matrix:
15 | os: [ubuntu-latest, macos-latest]
16 | runs-on: ${{ matrix.os }}
17 |
18 | steps:
19 | - name: Install current Bash on macOS
20 | if: runner.os == 'macOS'
21 | run: brew install bash
22 |
23 | - uses: actions/checkout@v4
24 | - uses: cachix/install-nix-action@v27
25 |
26 | - name: Build the project with Nix
27 | run: nix-build
28 |
29 | - name: Cache Cabal
30 | id: cache
31 | uses: actions/cache@v4
32 | with:
33 | path: |
34 | ~/.cabal
35 | dist-newstyle
36 | .devenv/profile
37 | key: ${{ runner.os }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/*.lock') }}-1 # modify the key if the cache is not working as expected
38 |
39 | - name: Update Cabal Hackage list
40 | # if: steps.cache.outputs.cache-hit != 'true'
41 | run: nix-shell --run "cabal update"
42 |
43 | - name: Cabal check cli
44 | run: nix-shell --run "cd cli; cabal check"
45 |
46 | - name: Cabal check core
47 | run: nix-shell --run "cd core; cabal check"
48 |
49 | - name: Build the project with cabal
50 | run: nix-shell --run "cabal build all"
51 |
52 | - name: Run HLint
53 | run: nix-shell --run "hlint ."
54 |
55 | - name: Check formatting
56 | run: nix-shell --run "fourmolu --mode check ."
57 |
58 | - name: Run core tests
59 | run: nix-shell --run "cabal test core"
60 |
61 | - name: Run CLI tests
62 | run: nix-shell --run "cabal test cli"
63 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Devenv
2 | .devenv*
3 | devenv.local.nix
4 |
5 | # direnv
6 | .direnv
7 |
8 | # pre-commit
9 | .pre-commit-config.yaml
10 | dist
11 | dist-*
12 | cabal-dev
13 | *.o
14 | *.hi
15 | *.hie
16 | *.chi
17 | *.chs.h
18 | *.dyn_o
19 | *.dyn_hi
20 | .hpc
21 | .hsenv
22 | .cabal-sandbox/
23 | cabal.sandbox.config
24 | *.prof
25 | *.aux
26 | *.hp
27 | *.eventlog
28 | .stack-work/
29 | cabal.project.local
30 | cabal.project.local~
31 | .HTF/
32 | .ghc.environment.*
33 | .aider*
34 |
35 | # hie cradle generated automatically
36 | hie.yaml
37 |
38 | nhout
39 | sandbox/*.cabal
40 | sandbox/*.nix
41 |
42 | example.cabal
43 | result
44 |
--------------------------------------------------------------------------------
/.hlint.yaml:
--------------------------------------------------------------------------------
1 | # HLint configuration file
2 | # https://github.com/ndmitchell/hlint
3 | ##########################
4 |
5 | # This file contains a template configuration file, which is typically
6 | # placed as .hlint.yaml in the root of your project
7 |
8 | # Specify additional command line arguments
9 | #
10 | - arguments: [--color, --cpp-simple, -XQuasiQuotes]
11 |
12 | # Control which extensions/flags/modules/functions can be used
13 | #
14 | # - extensions:
15 | # - default: false # all extension are banned by default
16 | # - name: [PatternGuards, ViewPatterns] # only these listed extensions can be used
17 | # - {name: CPP, within: CrossPlatform} # CPP can only be used in a given module
18 | #
19 | # - flags:
20 | # - {name: -w, within: []} # -w is allowed nowhere
21 | #
22 | # - modules:
23 | # - {name: [Data.Set, Data.HashSet], as: Set} # if you import Data.Set qualified, it must be as 'Set'
24 | # - {name: Control.Arrow, within: []} # Certain modules are banned entirely
25 | #
26 | # - functions:
27 | # - {name: (.), within: []} # Cannot use (.) at all
28 |
29 | # Add custom hints for this project
30 | #
31 | # Will suggest replacing "wibbleMany [myvar]" with "wibbleOne myvar"
32 | # - error: {lhs: "wibbleMany [x]", rhs: wibbleOne x}
33 | # - error: {lhs: "[x]", rhs: Array.wrap x}
34 |
35 | # The hints are named by the string they display in warning messages.
36 | # For example, if you see a warning starting like
37 | #
38 | # Main.hs:116:51: Warning: Redundant ==
39 | #
40 | # You can refer to that hint with `{name: Redundant ==}` (see below).
41 |
42 | # Turn on hints that are off by default
43 | #
44 | # Ban "module X(module X) where", to require a real export list
45 | # - warn: {name: Use explicit module export list}
46 | #
47 | # Replace a $ b $ c with a . b $ c
48 | # - group: {name: dollar, enabled: true}
49 | #
50 | # Generalise map to fmap, ++ to <>
51 | # - group: {name: generalise, enabled: true}
52 | #
53 | # Warn on use of partial functions
54 | # - group: {name: partial, enabled: true}
55 |
56 | # Ignore some builtin hints
57 | # - ignore: {name: Use let}
58 | # - ignore: {name: Use const, within: SpecialModule} # Only within certain modules
59 | - ignore: { name: Eta reduce }
60 | - ignore: { name: Redundant bracket }
61 | - ignore: { name: Use newtype instead of data }
62 | - ignore: { name: Use const }
63 | - ignore: { name: Avoid lambda using `infix` }
64 | - ignore: { name: Avoid lambda }
65 | - ignore: { name: Use /= }
66 | - ignore: { name: Use <$> }
67 | - ignore: { name: Replace case with maybe }
68 | - ignore: { name: Use when }
69 | - ignore: { name: Use lambda case }
70 | # Define some custom infix operators
71 | # - fixity: infixr 3 ~^#^~
72 |
73 | # To generate a suitable file for HLint do:
74 | # $ hlint --default > .hlint.yaml
75 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "streetsidesoftware.code-spell-checker",
4 | "haskell.haskell",
5 | "arrterian.nix-env-selector",
6 | "esbenp.prettier-vscode",
7 | "lunaryorn.hlint"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "absdir",
4 | "absfile",
5 | "Acculumator",
6 | "Appendable",
7 | "clippy",
8 | "Defaultable",
9 | "devenv",
10 | "dont",
11 | "foldl",
12 | "fprint",
13 | "GADT",
14 | "hlint",
15 | "Monoid",
16 | "Nanotime",
17 | "NEOHASKELL",
18 | "nhcore",
19 | "nixfmt",
20 | "nixpkgs",
21 | "NOINLINE",
22 | "optparse",
23 | "OVERLAPPABLE",
24 | "pkgs",
25 | "Posix",
26 | "reldir",
27 | "relfile",
28 | "Semigroup",
29 | "snoc",
30 | "typeclasses",
31 | "Unagi"
32 | ],
33 | "nixEnvSelector.nixFile": "${workspaceFolder}/shell.nix",
34 | // Haskell
35 | "haskell.manageHLS": "PATH",
36 | "[haskell]": {
37 | "editor.formatOnSave": true,
38 | "editor.defaultFormatter": "haskell.haskell"
39 | },
40 | "haskell.formattingProvider": "fourmolu",
41 | "haskell.plugin.fourmolu.config.external": true,
42 | // JSON
43 | "[json]": {
44 | "editor.defaultFormatter": "esbenp.prettier-vscode",
45 | "editor.formatOnSave": true
46 | },
47 | "[jsonc]": {
48 | "editor.defaultFormatter": "esbenp.prettier-vscode",
49 | "editor.formatOnSave": true
50 | },
51 | // Markdown
52 | "[markdown]": {
53 | "editor.defaultFormatter": "esbenp.prettier-vscode",
54 | "editor.formatOnSave": true
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "process",
6 | "command": "cabal",
7 | "args": ["--enable-nix", "build", "all"],
8 | "label": "cabal: build all",
9 | "presentation": {
10 | "echo": true,
11 | "reveal": "silent",
12 | "focus": false,
13 | "panel": "shared",
14 | "showReuseMessage": true,
15 | "clear": false
16 | },
17 | "group": {
18 | "kind": "build",
19 | "isDefault": true
20 | }
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
NeoHaskell
4 |
5 | NeoHaskell is a dialect of Haskell that is focused on newcomer-friendliness and productivity.
6 |
7 |
8 | It is designed to be easy to learn and use, while also being powerful enough to release your app with minimum effort and maximum confidence.
9 |
10 |
11 |
12 | ---
13 |
14 | # Welcome to the contributor guide
15 |
16 | If you want to learn about NeoHaskell itself, checkout
17 | [the NeoHaskell website](https://neohaskell.org).
18 |
19 | This guide is intended to streamline the process of
20 | contributing to the NeoHaskell tooling.
21 |
22 | The repository will be a mono-repo that contains all the
23 | different parts of NeoHaskell.
24 |
25 | ## Installing the required tools
26 |
27 | (This assumes that you're using MacOS, WSL2 or Linux)
28 |
29 | - Install [Nix](https://nixos.org/download/)
30 | - Run `nix-shell`
31 | - Run `cabal update && cabal build all`
32 |
33 | The recommended IDE for any NeoHaskell project is [Visual Studio Code](https://code.visualstudio.com/).
34 |
35 | ## Get the code
36 |
37 | - Fork this repository
38 | - `git clone `
39 | - `cd NeoHaskell && code .`
40 |
41 | ## Install the recommended extensions
42 |
43 | When opening the project for the first time, you will be prompted to install the recommended extensions, install them.
44 |
45 | ## Code Formatting
46 |
47 | This project uses the fourmolu formatter for consistent Haskell code styling. When using VS Code with the recommended extensions:
48 |
49 | - Code will automatically format on save
50 | - The formatter settings are controlled by the fourmolu.yaml file in the root directory
51 |
52 | ## Linting
53 |
54 | This project uses hlint, it will automatically be run in VSCode by the recommended extension.
55 | To run manually:
56 |
57 | ```sh
58 | hlint .
59 | ```
60 |
61 | ## Collaborate on Discord
62 |
63 | It's always better to hack with people, so why not join the [Discord server](https://discord.gg/invite/wDj3UYzec8)?
64 |
--------------------------------------------------------------------------------
/cabal.project:
--------------------------------------------------------------------------------
1 | packages:
2 | cli/*.cabal
3 | core/*.cabal
4 |
--------------------------------------------------------------------------------
/cli/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/cli/app/Main.hs:
--------------------------------------------------------------------------------
1 | module Main where
2 |
3 | import Core
4 | import Neo qualified
5 | import Task qualified
6 |
7 |
8 | main :: IO ()
9 | main = Task.runMain Neo.run
10 |
--------------------------------------------------------------------------------
/cli/default.nix:
--------------------------------------------------------------------------------
1 | { pkgs ? import ../nix/nixpkgs.nix { } }:
2 | pkgs.haskellPackages.developPackage {
3 | root = ./.;
4 | source-overrides = {
5 | nhcore = ../core;
6 | };
7 | modifier = drv: pkgs.haskell.lib.dontHaddock drv;
8 | }
9 |
--------------------------------------------------------------------------------
/cli/nhcli.cabal:
--------------------------------------------------------------------------------
1 | cabal-version: 3.4
2 | name: nhcli
3 | version: 0.6.0
4 | synopsis: Command Line Tool for NeoHaskell
5 | description: NeoHaskell is a dialect of Haskell that is focused on newcomer-friendliness and productivity.
6 | homepage: https://neohaskell.org
7 | license: Apache-2.0
8 | license-file: LICENSE
9 | author: Nikita Tchayka
10 | maintainer: nhlib@nickseagull.dev
11 | copyright:
12 | category: Development
13 | build-type: Simple
14 | -- extra-source-files:
15 |
16 | common common_cfg
17 | ghc-options: -Wall
18 | -threaded
19 | -fno-warn-partial-type-signatures
20 | -fno-warn-name-shadowing
21 | -Werror
22 |
23 | default-extensions:
24 | ApplicativeDo
25 | BlockArguments
26 | DataKinds
27 | NoImplicitPrelude
28 | TemplateHaskell
29 | DeriveDataTypeable
30 | QuasiQuotes
31 | QualifiedDo
32 | ImpredicativeTypes
33 | ImportQualifiedPost
34 | OverloadedStrings
35 | OverloadedLabels
36 | OverloadedRecordDot
37 | DuplicateRecordFields
38 | PackageImports
39 | NamedFieldPuns
40 | Strict
41 | TypeFamilies
42 | PartialTypeSignatures
43 |
44 | build-depends:
45 | nhcore,
46 |
47 |
48 |
49 | library
50 | import: common_cfg
51 | exposed-modules:
52 | Neo,
53 | Neo.Build,
54 | Neo.Build.Templates.Cabal,
55 | Neo.Build.Templates.Nix,
56 | Neo.Build.Templates.AppMain,
57 | Neo.Core,
58 | Neo.Core.ProjectConfiguration,
59 | Neo.New,
60 | Neo.New.Templates.NeoJson,
61 | Neo.New.Templates.MainModule,
62 | Neo.Run,
63 | -- other-modules:
64 | -- other-extensions:
65 | hs-source-dirs: src
66 | default-language: GHC2021
67 |
68 | executable neo
69 | import: common_cfg
70 | main-is: Main.hs
71 | -- other-modules:
72 | -- other-extensions:
73 | build-depends:
74 | nhcli
75 | hs-source-dirs: app
76 | default-language: GHC2021
77 |
78 | test-suite nhcli-test
79 | import: common_cfg
80 | default-language: GHC2021
81 | -- other-modules:
82 | -- other-extensions:
83 | type: exitcode-stdio-1.0
84 | hs-source-dirs: test
85 | main-is: Main.hs
86 | build-depends:
87 | nhcli
88 |
--------------------------------------------------------------------------------
/cli/src/Neo.hs:
--------------------------------------------------------------------------------
1 | {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
2 |
3 | module Neo (
4 | run,
5 | ) where
6 |
7 | import Array qualified
8 | import Command qualified
9 | import Console qualified
10 | import Core
11 | import File qualified
12 | import Json qualified
13 | import Neo.Build qualified as Build
14 | import Neo.New qualified as New
15 | import Neo.Run qualified as Run
16 | import Task qualified
17 | import Text qualified
18 |
19 |
20 | data CommonFlags = CommonFlags
21 | { projectFile :: Path
22 | }
23 | deriving (Show, Eq, Ord)
24 |
25 |
26 | data NeoCommand
27 | = Build CommonFlags
28 | | Run CommonFlags
29 | | New New.ProjectName
30 | deriving (Show, Eq, Ord)
31 |
32 |
33 | run :: Task Text Unit
34 | run = do
35 | let message1 = "⚠️ NEOHASKELL IS IN ITS EARLY DAYS ⚠️"
36 | let message2 = "HERE BE DRAGONS, BEWARE!"
37 | let msgLength = Text.length message1 + 4
38 | let paddedMessage1 = message1 |> Text.pad msgLength ' '
39 | let paddedMessage2 = message2 |> Text.pad msgLength ' '
40 | Console.print ""
41 | Console.print ""
42 | Console.print paddedMessage1
43 | Console.print paddedMessage2
44 | Console.print ""
45 | Console.print ""
46 | let parser =
47 | Command.CommandOptions
48 | { name = "neo",
49 | description = "NeoHaskell's console helper",
50 | version = Just [Core.version|0.6.0|],
51 | decoder = commandsParser
52 | }
53 | cmd <- Command.parseHandler parser
54 | handleCommand cmd
55 | |> Task.mapError
56 | \e ->
57 | [fmt|
58 | Neo: While running your command, I've encountered an error:
59 | {errorToText e}
60 |
61 | It looks like your last command failed. Remember, if it is taking
62 | you more than 15 minutes to figure it out, it is a bug in the system.
63 |
64 | Please go to:
65 |
66 | https://github.com/neohaskell/neohaskell/issues/new
67 |
68 | And report it. I'll be waiting for you.
69 | |]
70 |
71 |
72 | commandsParser :: Command.OptionsParser NeoCommand
73 | commandsParser = do
74 | let new =
75 | Command.CommandOptions
76 | { name = "new",
77 | description = "create a new project",
78 | version = Nothing,
79 | decoder = newParser
80 | }
81 | let build =
82 | Command.CommandOptions
83 | { name = "build",
84 | description = "build the project",
85 | version = Nothing,
86 | decoder = buildParser
87 | }
88 | let run =
89 | Command.CommandOptions
90 | { name = "run",
91 | description = "run the project",
92 | version = Nothing,
93 | decoder = runParser
94 | }
95 | Command.commands
96 | (Array.fromLinkedList [new, build, run])
97 |
98 |
99 | buildParser :: Command.OptionsParser NeoCommand
100 | buildParser = do
101 | common <- flagsParser
102 | pure (Build common)
103 |
104 |
105 | runParser :: Command.OptionsParser NeoCommand
106 | runParser = do
107 | common <- flagsParser
108 | pure (Run common)
109 |
110 |
111 | newParser :: Command.OptionsParser NeoCommand
112 | newParser = do
113 | projectName <- projectNameParser
114 | pure (New projectName)
115 |
116 |
117 | flagsParser :: Command.OptionsParser CommonFlags
118 | flagsParser = do
119 | projectFilePath <-
120 | Command.path
121 | Command.PathConfig
122 | { metavar = "PATH",
123 | short = 'c',
124 | help = "Path to the project configuration file",
125 | long = "projectConfig",
126 | value = Just [path|neo.json|]
127 | }
128 | pure (CommonFlags {projectFile = projectFilePath})
129 |
130 |
131 | projectNameParser :: Command.OptionsParser New.ProjectName
132 | projectNameParser = do
133 | projectName <-
134 | Command.text
135 | Command.TextConfig
136 | { metavar = "NAME",
137 | short = 'n',
138 | help = "Name of the project",
139 | long = "name",
140 | value = Nothing
141 | }
142 | pure (New.ProjectName projectName)
143 |
144 |
145 | data Error
146 | = BuildError Build.Error
147 | | RunError Run.Error
148 | | NewError New.Error
149 | | Other
150 |
151 |
152 | errorToText :: Error -> Text
153 | errorToText err =
154 | case err of
155 | BuildError buildError ->
156 | [fmt|Error building the project, check the logs above for more details.
157 |
158 |
159 | Here's some more info though:
160 |
161 | {toPrettyText buildError}|]
162 | RunError runError ->
163 | [fmt|Error running the project, check the logs above for more details.
164 |
165 |
166 | Here's some more info though:
167 |
168 | {toPrettyText runError}|]
169 | NewError newError ->
170 | [fmt|Error creating the project, check the logs above for more details.
171 |
172 |
173 | Here's some more info though:
174 |
175 | {toPrettyText newError}|]
176 | Other ->
177 | [fmt|An unknown error occurred, check the logs above for more details.|]
178 |
179 |
180 | handleCommand :: NeoCommand -> Task Error ()
181 | handleCommand command =
182 | case command of
183 | Build flags -> do
184 | txt <- File.readText flags.projectFile |> Task.mapError (\_ -> Other)
185 | case Json.decodeText txt of
186 | Err err -> panic err
187 | Ok config ->
188 | Build.handle config
189 | |> Task.mapError (\e -> BuildError e)
190 | Run flags -> do
191 | txt <- File.readText flags.projectFile |> Task.mapError (\_ -> Other)
192 | case Json.decodeText txt of
193 | Err err -> panic err
194 | Ok config -> do
195 | Build.handle config
196 | |> Task.mapError (\e -> BuildError e)
197 | Run.handle config
198 | |> Task.mapError (\e -> RunError e)
199 | New projectName -> do
200 | New.handle projectName
201 | |> Task.mapError (\e -> NewError e)
202 |
--------------------------------------------------------------------------------
/cli/src/Neo/Build.hs:
--------------------------------------------------------------------------------
1 | module Neo.Build (
2 | handle,
3 | Error (..),
4 | ) where
5 |
6 | import Array qualified
7 | import Directory qualified
8 | import File qualified
9 | import Maybe qualified
10 | import Neo.Build.Templates.AppMain qualified as AppMain
11 | import Neo.Build.Templates.Cabal qualified as Cabal
12 | import Neo.Build.Templates.Nix qualified as Nix
13 | import Neo.Core
14 | import Path qualified
15 | import Subprocess qualified
16 | import Task qualified
17 | import Text qualified
18 | import ToText (toText)
19 |
20 |
21 | data Error
22 | = NixFileError
23 | | CabalFileError
24 | | CustomError Text
25 | deriving (Show)
26 |
27 |
28 | handle :: ProjectConfiguration -> Task Error Unit
29 | handle config = do
30 | let haskellExtension = ".hs"
31 | let projectName = config.name
32 | let rootFolder = [path|nhout|]
33 | let nixFileName = [path|default.nix|]
34 | let cabalFileName =
35 | [fmt|{projectName}.cabal|]
36 | |> Path.fromText
37 | |> Maybe.getOrDie -- TODO: Make better error handling here
38 | let nixFile = Nix.template config
39 | let nixFilePath =
40 | Array.fromLinkedList [rootFolder, nixFileName]
41 | |> Path.joinPaths
42 | let cabalFilePath =
43 | Array.fromLinkedList [rootFolder, cabalFileName]
44 | |> Path.joinPaths
45 | let targetAppFolder =
46 | Array.fromLinkedList [rootFolder, "app"]
47 | |> Path.joinPaths
48 | let targetAppPath =
49 | Array.fromLinkedList [targetAppFolder, "Main.hs"]
50 | |> Path.joinPaths
51 | let targetSrcFolder =
52 | Array.fromLinkedList [rootFolder, "src"]
53 | |> Path.joinPaths
54 |
55 | Directory.copy [path|src|] targetSrcFolder
56 | |> Task.mapError (\e -> CustomError (toText e))
57 |
58 | filepaths <-
59 | Directory.walk [path|src|]
60 | |> Task.mapError (\_ -> CustomError "WALK ERROR")
61 |
62 | let haskellFiles = filepaths |> Array.takeIf (Path.endsWith haskellExtension)
63 |
64 | let convertToModule filepath = do
65 | let pathText = Path.toText filepath
66 | let pathWithoutExtension = Text.dropRight (Text.length haskellExtension) pathText
67 | let pathParts = Text.split "/" pathWithoutExtension
68 | pathParts |> Text.joinWith "."
69 |
70 | let modules = haskellFiles |> Array.map convertToModule
71 | let cabalFile = Cabal.template config modules
72 | let appMainFile = AppMain.template config
73 |
74 | Directory.create rootFolder
75 | |> Task.mapError (\_ -> [fmt|Could not create directory {Path.toText rootFolder}|] |> CustomError)
76 |
77 | Directory.create targetAppFolder
78 | |> Task.mapError (\_ -> [fmt|Could not create directory {Path.toText targetAppFolder}|] |> CustomError)
79 |
80 | File.writeText nixFilePath nixFile
81 | |> Task.mapError (\_ -> NixFileError)
82 |
83 | File.writeText cabalFilePath cabalFile
84 | |> Task.mapError (\_ -> CabalFileError)
85 |
86 | File.writeText targetAppPath appMainFile
87 | |> Task.mapError (\_ -> CustomError "Could not write app main file")
88 |
89 | -- FIXME: Create another thread that renders the output of the build via streaming.
90 | -- As right now there's no output at all
91 | completion <- Subprocess.openInherit "nix-build" (Array.fromLinkedList []) rootFolder Subprocess.InheritBOTH
92 | if completion.exitCode != 0
93 | then errorOut completion.stderr
94 | else print completion.stdout
95 |
96 |
97 | errorOut :: Text -> Task Error _
98 | errorOut err =
99 | [fmt|Oops the build failed:
100 | {err}|]
101 | |> CustomError
102 | |> Task.throw
103 |
--------------------------------------------------------------------------------
/cli/src/Neo/Build/Templates/AppMain.hs:
--------------------------------------------------------------------------------
1 | module Neo.Build.Templates.AppMain where
2 |
3 | import Core
4 | import Neo.Core
5 | import Text qualified
6 |
7 |
8 | template :: ProjectConfiguration -> Text
9 | template
10 | ProjectConfiguration {name} = do
11 | let mainModuleName = Text.toPascalCase name
12 | [fmt|module Main where
13 |
14 | import Core
15 | import qualified Task
16 | import qualified {mainModuleName}
17 |
18 | main :: IO ()
19 | main = Task.runOrPanic {mainModuleName}.run
20 | |]
21 |
--------------------------------------------------------------------------------
/cli/src/Neo/Build/Templates/Cabal.hs:
--------------------------------------------------------------------------------
1 | module Neo.Build.Templates.Cabal (
2 | template,
3 | ) where
4 |
5 | import Array qualified
6 | import Map qualified
7 | import Neo.Core
8 | import Text qualified
9 | import Version qualified
10 |
11 |
12 | -- FIXME: Bring here discovered modules in the folders
13 | template :: ProjectConfiguration -> Array Text -> Text
14 | template
15 | ProjectConfiguration {name, version, description, license, author, dependencies}
16 | modules = do
17 | let execName = Text.toKebabCase name
18 | -- FIXME: Move onto a separate version handling module
19 | let vText = Version.toText version
20 | let makeDep (k, v)
21 | | v |> Text.trim |> Text.startsWith "^" = [fmt|{k} ^>= {v |> Text.replace "^" ""}|]
22 | | otherwise = [fmt|{k} == {v}|]
23 | let deps =
24 | dependencies
25 | |> Map.entries
26 | |> Array.map makeDep
27 | |> Array.push [fmt|nhcore|]
28 | |> Text.joinWith ", "
29 |
30 | let mods = modules |> Text.joinWith ", "
31 | [fmt|cabal-version: 3.4
32 | -- THIS CABAL FILE IS AUTOGENERATED BY `neo`, THE NEOHASKELL CLI TOOL.
33 | -- YOU SHOULD NOT MODIFY IT, AS IT WILL BE REGENERATED NEXT TIME `neo` RUNS.
34 | --
35 | -- IF YOU HAVE A SPECIFIC NEED TO MODIFY THIS
36 | -- FILE, PLEASE STATE SO EITHER IN A GITHUB
37 | -- ISSUE: https://github.com/NeoHaskell/NeoHaskell/issues
38 | -- OR IN THE NEOHASKELL DISCORD SERVER.
39 | -- YOU CAN JOIN IT THROUGH THE LINK IN
40 | -- https://neohaskell.org
41 | name: {name}
42 | version: {vText}
43 | synopsis: {description}
44 | license: {license}
45 | author: {author}
46 |
47 | common common_cfg
48 | ghc-options: -Wall
49 | -Werror
50 | -threaded
51 | default-language: GHC2021
52 | default-extensions:
53 | ApplicativeDo
54 | BlockArguments
55 | DataKinds
56 | NoImplicitPrelude
57 | TemplateHaskell
58 | DeriveDataTypeable
59 | QuasiQuotes
60 | QualifiedDo
61 | ImpredicativeTypes
62 | ImportQualifiedPost
63 | OverloadedStrings
64 | OverloadedLabels
65 | OverloadedRecordDot
66 | DuplicateRecordFields
67 | PackageImports
68 | NamedFieldPuns
69 | Strict
70 | TypeFamilies
71 |
72 | build-depends:
73 | {deps}
74 |
75 | library
76 | import: common_cfg
77 | exposed-modules:
78 | {mods}
79 | hs-source-dirs: src
80 |
81 | executable {execName}
82 | import: common_cfg
83 | main-is: Main.hs
84 | build-depends:
85 | {name}
86 | hs-source-dirs: app
87 |
88 | |]
89 |
--------------------------------------------------------------------------------
/cli/src/Neo/Build/Templates/Nix.hs:
--------------------------------------------------------------------------------
1 | module Neo.Build.Templates.Nix (
2 | template,
3 | ) where
4 |
5 | import Neo.Core
6 |
7 |
8 | -- FIXME: Use file-embed instead of duplicating this and what's in `nix/nixpkgs.nix`
9 | pinnedNixpkgs :: Text
10 | pinnedNixpkgs =
11 | [fmt|import (builtins.fetchTarball {{
12 | name = "haskell-fixes";
13 | url = "https://github.com/nixos/nixpkgs/archive/c95b3e3904d1c3138cafab0ddfbc08336128f664.tar.gz";
14 | sha256 = "03b5i7almr4v68b677qqnbyvrmqdxq02gks7q1jr6kfm2j51bgw5";
15 | }})
16 | |]
17 |
18 |
19 | template :: ProjectConfiguration -> Text
20 | template ProjectConfiguration {name} =
21 | {-
22 | https://github.com/NixOS/nixpkgs/blob/777a9707e72e6dbbbdf9033c44f237154c64e9f7/pkgs/development/haskell-modules/make-package-set.nix#L227-L254
23 |
24 | Also: https://srid.ca/haskell-nix
25 |
26 | TODO: Consider dropping developPackage in favor of making the package
27 | out of neo.json. Although maybe that could lead to IDEs not being able
28 | to find the source code.
29 |
30 | TODO: Figure out how to do caching here
31 | -}
32 | [fmt|{{ pkgs ? ({pinnedNixpkgs}) {{}} }}:
33 | let
34 | neoHaskellGitHub = builtins.fetchTarball
35 | "https://github.com/NeoHaskell/NeoHaskell/archive/refs/heads/main.tar.gz";
36 | in
37 | pkgs.haskellPackages.developPackage {{
38 | name = "{name}";
39 | root = ./.;
40 | returnShellEnv = false;
41 | source-overrides = {{
42 | nhcore = "${{neoHaskellGitHub}}/core";
43 | }};
44 | }} // {{ name = "{name}"; }}
45 | |]
46 |
--------------------------------------------------------------------------------
/cli/src/Neo/Core.hs:
--------------------------------------------------------------------------------
1 | module Neo.Core (
2 | ProjectConfiguration (..),
3 | module Core,
4 | ) where
5 |
6 | import Core
7 | import Neo.Core.ProjectConfiguration (ProjectConfiguration (..))
8 |
9 |
--------------------------------------------------------------------------------
/cli/src/Neo/Core/ProjectConfiguration.hs:
--------------------------------------------------------------------------------
1 | module Neo.Core.ProjectConfiguration (
2 | ProjectConfiguration (..),
3 | fromText,
4 | ) where
5 |
6 | import Core
7 | import Json qualified
8 |
9 |
10 | data ProjectConfiguration = ProjectConfiguration
11 | { name :: Text,
12 | version :: Version,
13 | description :: Text,
14 | author :: Text,
15 | license :: Text,
16 | dependencies :: Map Text Text
17 | }
18 | deriving (Show, Eq, Ord, Generic)
19 |
20 |
21 | -- | We allow the `ProjectConfiguration` type to be converted from JSON.
22 | instance Json.FromJSON ProjectConfiguration
23 |
24 |
25 | -- | We allow the `ProjectConfiguration` type to be converted to JSON.
26 | instance Json.ToJSON ProjectConfiguration
27 |
28 |
29 | -- | The `ProjectConfiguration.fromText` function allows you to convert a JSON
30 | -- `Text` value to a `ProjectConfiguration` value.
31 | --
32 | -- >>> fromText "{\"name\":\"neo\",\"version\":\"0.5.0\",\"description\":\"NeoHaskell's console helper\",\"author\":\"NeoHaskell\",\"license\":\"MIT\"}"
33 | -- Ok (ProjectConfiguration {name = "neo", version = [version|0.5.0|], description = "NeoHaskell's console helper", author = "NeoHaskell", license = "MIT"})
34 | --
35 | -- >>> fromText "some invalid json"
36 | -- Err "Error in $: not enough input"
37 | fromText :: Text -> Result Text ProjectConfiguration
38 | fromText someText = Json.decodeText someText
39 |
--------------------------------------------------------------------------------
/cli/src/Neo/New.hs:
--------------------------------------------------------------------------------
1 | module Neo.New (
2 | handle,
3 | Error (..),
4 | ProjectName (..),
5 | ) where
6 |
7 | import Array qualified
8 | import Directory qualified
9 | import File qualified
10 | import Maybe qualified
11 | import Neo.Core
12 | import Neo.New.Templates.MainModule qualified as MainModule
13 | import Neo.New.Templates.NeoJson qualified as NeoJson
14 | import Path qualified
15 | import Task qualified
16 | import Text qualified
17 |
18 |
19 | newtype ProjectName = ProjectName Text
20 | deriving (Show, Eq, Ord)
21 |
22 |
23 | data Error
24 | = CabalFileError
25 | | CustomError Text
26 | deriving (Show)
27 |
28 |
29 | handle :: ProjectName -> Task Error Unit
30 | handle (ProjectName projectName) = do
31 | let kebabName = Text.toKebabCase projectName
32 | let projectDir =
33 | kebabName
34 | |> Text.toLower
35 | |> Path.fromText
36 | |> Maybe.getOrDie -- TODO: Handle this better
37 | let srcDir =
38 | Array.fromLinkedList [projectDir, "src"]
39 | |> Path.joinPaths
40 |
41 | let moduleName =
42 | projectName
43 | |> Text.toPascalCase
44 | let moduleFileName =
45 | [fmt|{moduleName}.hs|]
46 | |> Path.fromText
47 | |> Maybe.getOrDie -- TODO: Handle this better
48 | let moduleFilePath =
49 | Array.fromLinkedList [srcDir, moduleFileName]
50 | |> Path.joinPaths
51 | let configFileName = [path|neo.json|]
52 | let configFilePath =
53 | Array.fromLinkedList [projectDir, configFileName]
54 | |> Path.joinPaths
55 |
56 | Directory.create projectDir
57 | |> Task.mapError (\_ -> [fmt|Could not create directory {Path.toText projectDir}|] |> CustomError)
58 |
59 | File.writeText configFilePath (NeoJson.template kebabName)
60 | |> Task.mapError (\_ -> CustomError "Could not write config file")
61 |
62 | Directory.create srcDir
63 | |> Task.mapError (\_ -> [fmt|Could not create directory {Path.toText srcDir}|] |> CustomError)
64 |
65 | File.writeText moduleFilePath (MainModule.template moduleName)
66 | |> Task.mapError (\_ -> CustomError "Could not write module file")
67 |
68 | print
69 | [fmt|Created project {projectName} at ./{Path.toText projectDir}
70 |
71 | To build your project:
72 | cd {Path.toText projectDir}
73 | neo build
74 |
75 | To run your project:
76 | cd {Path.toText projectDir}
77 | neo run
78 | |]
79 |
--------------------------------------------------------------------------------
/cli/src/Neo/New/Templates/MainModule.hs:
--------------------------------------------------------------------------------
1 | module Neo.New.Templates.MainModule where
2 |
3 | import Core
4 |
5 |
6 | template :: Text -> Text
7 | template moduleName = do
8 | [fmt|module {moduleName} where
9 |
10 | import Core
11 |
12 |
13 | run :: Task Text ()
14 | run = do
15 | print "Let's go NeoHaskell! ⏩"
16 | |]
17 |
--------------------------------------------------------------------------------
/cli/src/Neo/New/Templates/NeoJson.hs:
--------------------------------------------------------------------------------
1 | module Neo.New.Templates.NeoJson where
2 |
3 | import Core
4 |
5 |
6 | template :: Text -> Text
7 | template projectName = do
8 | [fmt|{{
9 | "name": "{projectName}",
10 | "version": "0.0.1",
11 | "description": "Your project's description",
12 | "author": "Your Name",
13 | "license": "ISC",
14 | "dependencies": {{
15 | }}
16 | }}|]
17 |
--------------------------------------------------------------------------------
/cli/src/Neo/Run.hs:
--------------------------------------------------------------------------------
1 | module Neo.Run (
2 | handle,
3 | Error (..),
4 | ) where
5 |
6 | import Array qualified
7 | import Neo.Core
8 | import Subprocess qualified
9 | import Task qualified
10 |
11 |
12 | data Error
13 | = NixFileError
14 | | CabalFileError
15 | | CustomError Text
16 | deriving (Show)
17 |
18 |
19 | handle :: ProjectConfiguration -> Task Error Unit
20 | handle config = do
21 | let projectName = config.name
22 | let rootFolder = [path|nhout|]
23 | completion <-
24 | Subprocess.openInherit [fmt|./result/bin/{projectName}|] (Array.fromLinkedList []) rootFolder Subprocess.InheritBOTH
25 | if completion.exitCode != 0
26 | then errorOut completion.stderr
27 | else print completion.stdout
28 |
29 |
30 | errorOut :: Text -> Task Error _
31 | errorOut err =
32 | [fmt|Oops running failed:
33 | {err}|]
34 | |> CustomError
35 | |> Task.throw
36 |
--------------------------------------------------------------------------------
/cli/test/Main.hs:
--------------------------------------------------------------------------------
1 | module Main (main) where
2 |
3 | import Core
4 |
5 |
6 | main :: IO ()
7 | main = log "Test suite not yet implemented."
8 |
--------------------------------------------------------------------------------
/context/code-style.md:
--------------------------------------------------------------------------------
1 | # NeoHaskell Code Style
2 |
3 | Code written in this codebase must follow the NeoHaskell code style.
4 |
5 | The NeoHaskell code style is as follows:
6 |
7 | - Don't include it into the file, but `NoImplicitPrelude` is enabled, take that into account.
8 | - Primitive types like`Int`, `Double`, `Char`, `Bool`, etc. are imported from their own NeoHaskell modules, which are named as the type itself. Not `Data.*`.
9 | - When importing collections, you will import them from the NeoHaskell module too, e.g. `Vector` instead of `Data.Vector` and `Maybe` instead of `Data.Maybe`.
10 | - Type parameters in functions will be **always** defined in a `forall` clause.
11 | - Type parameters will always be named as their intent, and not with short letters. For example, instead of `Array a`, one will write `Array value`.
12 | - Modules won't be imported unqualified unless it is for types or classes.
13 | - Modules will always be imported qualified to use functions from them.
14 | - Modules will be always imported qualified with a meaningful name. For example, `Data.Text` will be imported as `Text` and never as `T`. `GHC.Prim` will be imported as `Prim` and not as `P`.
15 | - Code **WILL NEVER** be written as point-free style, and instead it will always be typed as explicit function application.
16 | - The operators `$` and `.` make an unnatural switch of reading order, the `&` is **ALWAYS** used instead of those.
17 | - Modules from `base` will always be imported with a `Ghc` prefix in their qualification, in order to explicitly set boundaries.
18 | - Functions will always have doc comments as explained in [the documentation.md file](./documentation.md).
19 | - When relying on complex math or syntactic concepts of Haskell which could make some junior TypeScript dev not understand the code, a comment will be left in order to improve that. E.g. `-- TODO: Figure out a way of improve this API`.
20 |
--------------------------------------------------------------------------------
/context/collections.md:
--------------------------------------------------------------------------------
1 | # How to write collection type modules
2 |
3 | Collections are a key part of functional programming, and their API should be standard and compatible is a key part
4 | of the developer experience behind NeoHaskell.
5 |
6 | All collection types should define the following functions in order to
7 | maintain a cohesion between using them especially when one wants to
8 | migrate from one to another (e.g. from an Array to a Stream).
9 |
10 | The collection module will be imported qualified so the functions should
11 | never include the name of the type as a prefix or suffix.
12 |
13 | Note that the implemented functions MUST include the doc comments as per [doc comments file](./documentation.md).
14 |
15 | ## Constructors
16 |
17 | - `empty`: Constructs an empty collection
18 |
19 | - `yield`: Constructs a collection of one element
20 |
21 | ## Basic functions
22 |
23 | - `isEmpty`:
24 | - `length`:
25 | - `append`: Appends an element to the end of the collection
26 | - `flatten`:
27 | - `map`: Applies a function to each of the elements
28 | - `takeIf`: Akin to the common filter, takes element if the predicate is true
29 | - `reduce`:
30 | - `reduceFromRight`: Reduce, but starting from the end of the collection
31 | - `dropIf`: The opposite of takeIf, drops elements if the predicate is false
32 | - `mapMaybe`: Like map, but dropping the element if the function returns Nothing
33 | - `dropNones`: Filters out the Nothings out of a collection
34 |
35 | ## Transformations
36 |
37 | - `reverse`:
38 | - `intersperse`: Puts an element between each element
39 |
40 | ## Subarrays
41 |
42 | - `slice`:
43 | - `take`: Takes the first N elements of the collection
44 | - `drop`: Drops the first N elements of the collection
45 | - `splitAt`:
46 |
47 | ## Searching
48 |
49 | - `contains`:
50 | - `doesNotContain`:
51 | - `findIndex`:
52 |
53 | ## Accessing elements
54 |
55 | - `get`: Gets a specific element by index, returning a Maybe
56 | - `first`:
57 | - `last`:
58 | - `allButFirst`:
59 | - `allButLast`:
60 |
61 | ## Zipping and unzipping
62 |
63 | - `pairWith`:
64 | - `combineWith`:
65 | - `splitPairs`:
66 |
67 | ## Special reduces
68 |
69 | - `allTrue`: Reduces the collection, ensuring that all elements are true
70 | - `anyTrue`: Same but only for any
71 | - `anySatisfy`: Like the above but for predicates
72 | - `allSatisfy`:
73 |
74 | ## Set operations
75 |
76 | - `union`:
77 | - `intersect`:
78 | - `difference`:
79 |
80 | ## Additional operations
81 |
82 | - `sort`:
83 | - `sortBy`:
84 | - `deduplicate`:
85 |
--------------------------------------------------------------------------------
/context/documentation.md:
--------------------------------------------------------------------------------
1 | # How to write documentation comments
2 |
3 | All documentation comments (doc-comments) should be clear and concise,
4 | they shouldn't use Haskell-specific terminology, and instead, they should
5 | rely on other concepts of the current codebase (the NeoHaskell codebase).
6 |
7 | The comments should be written in a way that is familiar to a junior TypeScript developer
8 | TypeScript developer, using concepts from TypeScript as an anchor to
9 | explain things better.
10 |
11 | If the code is a function, it should include a doctest, which looks like
12 | this:
13 |
14 | ```hs
15 | -- >>> fib 10
16 | -- 55
17 | ```
18 |
19 | Even better, if the function has a property that can be established and
20 | verified, the doctest will define a property based doctest like so:
21 |
22 | ```hs
23 | -- prop> \(Small n) -> fib n == fib (n + 2) - fib (n + 1)
24 | ```
25 |
26 | Comments will use Markdown format instead of Haddock format.
27 |
--------------------------------------------------------------------------------
/core/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2024-present Nikita Tchayka
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
15 | The NeoHaskell core library is based on and/or incorporates `nri-prelude`, which itself is a derivative of `elm-core`. Both `nri-prelude` and `elm-core` are provided under the BSD-3-Clause license. This Derived Work has been modified to meet the specific needs of the project, and such modifications are provided under the Apache License 2.0.
16 |
17 | ## Elm License
18 |
19 | Copyright 2014-present Evan Czaplicki
20 |
21 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
22 |
23 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
24 |
25 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
26 |
27 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
28 |
29 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/core/concurrency/AsyncIO.hs:
--------------------------------------------------------------------------------
1 | module AsyncIO (AsyncIO, run, waitFor, sleep, process, waitAnyCancel, withRecovery, cancel) where
2 |
3 | import Array (Array)
4 | import Array qualified
5 | import Basics
6 | import Control.Concurrent qualified as Ghc
7 | import Control.Concurrent.Async qualified as GhcAsync
8 | import Data.Either qualified as Either
9 | import IO (IO)
10 | import Result (Result)
11 | import Result qualified
12 |
13 |
14 | type AsyncIO result = GhcAsync.Async result
15 |
16 |
17 | run :: IO result -> IO (AsyncIO result)
18 | run = GhcAsync.async
19 |
20 |
21 | waitFor :: AsyncIO result -> IO result
22 | waitFor = GhcAsync.wait
23 |
24 |
25 | process :: IO a -> (AsyncIO a -> IO b) -> IO b
26 | process = GhcAsync.withAsync
27 |
28 |
29 | sleep :: Int -> IO Unit
30 | sleep milliseconds = Ghc.threadDelay (milliseconds * 1000)
31 |
32 |
33 | withRecovery :: IO error -> IO result -> IO (Result error result)
34 | withRecovery errorIO resultIO = do
35 | result <- GhcAsync.race errorIO resultIO
36 | case result of
37 | Either.Left a -> pure (Result.Err a)
38 | Either.Right a -> pure (Result.Ok a)
39 |
40 |
41 | waitAnyCancel :: Array (AsyncIO a) -> IO (AsyncIO a, a)
42 | waitAnyCancel arr = do
43 | let asyncList =
44 | Array.toLinkedList arr
45 | (async, result) <- GhcAsync.waitAnyCancel asyncList
46 | pure (async, result)
47 |
48 |
49 | cancel :: AsyncIO a -> IO ()
50 | cancel = GhcAsync.cancel
51 |
--------------------------------------------------------------------------------
/core/concurrency/Channel.hs:
--------------------------------------------------------------------------------
1 | module Channel (Channel, new, read, write) where
2 |
3 | import Basics
4 | import Control.Concurrent.Chan.Unagi qualified as Unagi
5 | import IO (IO)
6 | import ToText (Show (..))
7 |
8 |
9 | data Channel value = Channel
10 | { outChannel :: Unagi.OutChan value,
11 | inChannel :: Unagi.InChan value
12 | }
13 | deriving (Show)
14 |
15 |
16 | instance Show (Unagi.OutChan value) where
17 | show _ = "[OutChan]"
18 |
19 |
20 | instance Show (Unagi.InChan value) where
21 | show _ = "[InChan]"
22 |
23 |
24 | new :: IO (Channel value)
25 | new = do
26 | (in_, out) <- Unagi.newChan
27 | pure Channel {outChannel = out, inChannel = in_}
28 |
29 |
30 | read :: Channel value -> IO value
31 | read self = Unagi.readChan (self.outChannel)
32 |
33 |
34 | write :: value -> Channel value -> IO ()
35 | write value self = Unagi.writeChan (self.inChannel) value
36 |
--------------------------------------------------------------------------------
/core/concurrency/ConcurrentVar.hs:
--------------------------------------------------------------------------------
1 | module ConcurrentVar (
2 | ConcurrentVar,
3 | new,
4 | get,
5 | set,
6 | peek,
7 | ) where
8 |
9 | import Basics
10 | import Control.Concurrent.MVar qualified as GHC
11 | import IO (IO)
12 |
13 |
14 | newtype ConcurrentVar value = ConcurrentVar (GHC.MVar value)
15 |
16 |
17 | new :: IO (ConcurrentVar value)
18 | new = do
19 | ref <- GHC.newEmptyMVar
20 | pure (ConcurrentVar ref)
21 |
22 |
23 | get :: ConcurrentVar value -> IO value
24 | get (ConcurrentVar ref) = GHC.takeMVar ref
25 |
26 |
27 | peek :: ConcurrentVar value -> IO value
28 | peek (ConcurrentVar ref) = GHC.readMVar ref
29 |
30 |
31 | set :: value -> ConcurrentVar value -> IO ()
32 | set value (ConcurrentVar ref) = GHC.putMVar ref value
33 |
--------------------------------------------------------------------------------
/core/core/Accumulator.hs:
--------------------------------------------------------------------------------
1 | module Accumulator (
2 | Accumulator,
3 | AccumulatorDsl (..),
4 | push,
5 | accumulate,
6 | update,
7 | andThen,
8 | yield,
9 | ) where
10 |
11 | import Appendable
12 | import Basics
13 | import Control.Monad.Trans.State qualified as GhcState
14 | import Default
15 | import Thenable qualified
16 |
17 |
18 | -- | `Accumulator` is a type that allows to accumulate values in
19 | -- a monadic way. As long as the content of the accumulator implements
20 | -- the `Combinable` and `HasDefault` traits, it is possible to use the `do` notation to
21 | -- accumulate values.
22 | --
23 | -- Example:
24 | --
25 | -- ```haskell
26 | -- foo :: Array Int
27 | -- foo = Accumulator.accumulate do
28 | -- Accumulator.push [1]
29 | -- Accumulator.push [2]
30 | -- Accumulator.push [3]
31 | -- ```
32 | type Accumulator value = AccumulatorDsl value ()
33 |
34 |
35 | data AccumulatorDsl someType result = AccumulatorDsl
36 | { value :: GhcState.State someType result
37 | }
38 |
39 |
40 | andThen ::
41 | (input -> AccumulatorDsl someType output) -> AccumulatorDsl someType input -> AccumulatorDsl someType output
42 | andThen callback self =
43 | self.value
44 | |> Thenable.andThen (callback .> value)
45 | |> AccumulatorDsl
46 |
47 |
48 | yield :: value -> AccumulatorDsl someType value
49 | yield value =
50 | Thenable.yield value
51 | |> AccumulatorDsl
52 |
53 |
54 | -- | Pushes a value into the accumulator.
55 | push :: (Appendable value) => value -> Accumulator value
56 | push value = do
57 | let pushToState accumulated = accumulated ++ value
58 | AccumulatorDsl (GhcState.modify pushToState)
59 |
60 |
61 | -- | Updates the accumulator with a callback.
62 | update :: (value -> value) -> Accumulator value
63 | update callback = do
64 | let updateState state = state |> callback
65 | AccumulatorDsl (GhcState.modify updateState)
66 |
67 |
68 | accumulate :: (Default value) => Accumulator value -> value
69 | accumulate (AccumulatorDsl state) = GhcState.execState state defaultValue
70 |
--------------------------------------------------------------------------------
/core/core/Array.hs:
--------------------------------------------------------------------------------
1 | -- | Fast immutable arrays. The elements in an array must have the same type.
2 | module Array (
3 | -- * Arrays
4 | Array,
5 |
6 | -- * Creation
7 | empty,
8 | initialize,
9 | repeat,
10 | wrap,
11 | fromLinkedList,
12 |
13 | -- * Query
14 | isEmpty,
15 | length,
16 | get,
17 |
18 | -- * Manipulate
19 | set,
20 | push,
21 | append,
22 | slice,
23 |
24 | -- * LinkedLists
25 | toLinkedList,
26 | toIndexedLinkedList,
27 |
28 | -- * Transform
29 | map,
30 | indexedMap,
31 | foldr,
32 | foldl,
33 | takeIf,
34 | dropIf,
35 | flatMap,
36 | foldM,
37 | dropWhile,
38 | takeWhile,
39 |
40 | -- * Partitioning?
41 | partitionBy,
42 | splitFirst,
43 | any,
44 |
45 | -- * Compatibility
46 | unwrap,
47 | ) where
48 |
49 | import Basics
50 | import Data.Foldable qualified
51 | import Data.Vector ((!?), (++), (//))
52 | import Data.Vector qualified
53 | import GHC.IsList qualified as GHC
54 | import IO (IO)
55 | import LinkedList (LinkedList)
56 | import LinkedList qualified
57 | import Maybe (Maybe (..))
58 | import Test.QuickCheck qualified as QuickCheck
59 | import Tuple qualified
60 | import Prelude qualified
61 |
62 |
63 | -- | Representation of fast immutable arrays. You can create arrays of integers
64 | -- (@Array Int@) or strings (@Array String@) or any other type of value you can
65 | -- dream up.
66 | newtype Array a = Array (Data.Vector.Vector a)
67 | deriving (Prelude.Eq, Prelude.Show, Prelude.Ord, Generic)
68 |
69 |
70 | instance (QuickCheck.Arbitrary a) => QuickCheck.Arbitrary (Array a) where
71 | arbitrary = do
72 | list <- QuickCheck.arbitrary
73 | pure (fromLinkedList list)
74 |
75 |
76 | instance GHC.IsList (Array a) where
77 | type Item (Array a) = a
78 | fromList = Basics.fromList
79 | toList = toLinkedList
80 |
81 |
82 | -- | Helper function to unwrap an array
83 | unwrap :: Array a -> Data.Vector.Vector a
84 | unwrap (Array v) = v
85 |
86 |
87 | -- | Return an empty array.
88 | --
89 | -- > length empty == 0
90 | empty :: Array a
91 | empty =
92 | Array Data.Vector.empty
93 |
94 |
95 | -- | Determine if an array is empty.
96 | --
97 | -- > isEmpty empty == True
98 | isEmpty :: Array a -> Bool
99 | isEmpty = unwrap .> Data.Vector.null
100 |
101 |
102 | -- | Return the length of an array.
103 | --
104 | -- > length (fromLinkedList [1,2,3]) == 3
105 | length :: Array a -> Int
106 | length =
107 | unwrap
108 | .> Data.Vector.length
109 | .> Prelude.fromIntegral
110 |
111 |
112 | -- | Initialize an array. @initialize n f@ creates an array of length @n@ with
113 | -- the element at index @i@ initialized to the result of @(f i)@.
114 | --
115 | -- > initialize 4 identity == fromLinkedList [0,1,2,3]
116 | -- > initialize 4 (\n -> n*n) == fromLinkedList [0,1,4,9]
117 | -- > initialize 4 (always 0) == fromLinkedList [0,0,0,0]
118 | initialize :: Int -> (Int -> a) -> Array a
119 | initialize n f =
120 | Array
121 | <| Data.Vector.generate
122 | (Prelude.fromIntegral n)
123 | (Prelude.fromIntegral .> f)
124 |
125 |
126 | -- | Creates an array with a given length, filled with a default element.
127 | --
128 | -- > repeat 5 0 == fromLinkedList [0,0,0,0,0]
129 | -- > repeat 3 "cat" == fromLinkedList ["cat","cat","cat"]
130 | --
131 | -- Notice that @repeat 3 x@ is the same as @initialize 3 (always x)@.
132 | repeat :: Int -> a -> Array a
133 | repeat n element =
134 | Array
135 | <| Data.Vector.replicate (Prelude.fromIntegral n) element
136 |
137 |
138 | -- | Wraps an element into an array
139 | wrap :: a -> Array a
140 | wrap element = fromLinkedList [element]
141 |
142 |
143 | -- | Create an array from a 'LinkedList'.
144 | fromLinkedList :: LinkedList a -> Array a
145 | fromLinkedList =
146 | Data.Vector.fromList .> Array
147 |
148 |
149 | -- | Return @Just@ the element at the index or @Nothing@ if the index is out of range.
150 | --
151 | -- > get 0 (fromLinkedList [0,1,2]) == Just 0
152 | -- > get 2 (fromLinkedList [0,1,2]) == Just 2
153 | -- > get 5 (fromLinkedList [0,1,2]) == Nothing
154 | -- > get (-1) (fromLinkedList [0,1,2]) == Nothing
155 | get :: Int -> Array a -> Maybe a
156 | get i array =
157 | unwrap array !? Prelude.fromIntegral i
158 |
159 |
160 | -- | Set the element at a particular index. Returns an updated array.
161 | --
162 | -- If the index is out of range, the array is unaltered.
163 | --
164 | -- > set 1 7 (fromLinkedList [1,2,3]) == fromLinkedList [1,7,3]
165 | set :: Int -> a -> Array a -> Array a
166 | set i value array = Array result
167 | where
168 | len = length array
169 | vector = unwrap array
170 | result
171 | | 0 <= i && i < len = vector Data.Vector.// [(Prelude.fromIntegral i, value)]
172 | | otherwise = vector
173 |
174 |
175 | -- | Push an element onto the end of an array.
176 | --
177 | -- > push 3 (fromLinkedList [1,2]) == fromLinkedList [1,2,3]
178 | push :: a -> Array a -> Array a
179 | push a (Array vector) =
180 | Array (Data.Vector.snoc vector a)
181 |
182 |
183 | -- | Create a list of elements from an array.
184 | --
185 | -- > toLinkedList (fromLinkedList [3,5,8]) == [3,5,8]
186 | toLinkedList :: Array a -> LinkedList a
187 | toLinkedList = unwrap .> Data.Vector.toList
188 |
189 |
190 | -- | Create an indexed list from an array. Each element of the array will be
191 | -- paired with its index.
192 | --
193 | -- > toIndexedLinkedList (fromLinkedList ["cat","dog"]) == [(0,"cat"), (1,"dog")]
194 | toIndexedLinkedList :: Array a -> LinkedList (Int, a)
195 | toIndexedLinkedList =
196 | unwrap
197 | .> Data.Vector.indexed
198 | .> Data.Vector.toList
199 | .> LinkedList.map (Tuple.mapFirst Prelude.fromIntegral)
200 |
201 |
202 | -- | Reduce an array from the right. Read @foldr@ as fold from the right.
203 | --
204 | -- > foldr (+) 0 (repeat 3 5) == 15
205 | foldr :: (a -> b -> b) -> b -> Array a -> b
206 | foldr f value array = Prelude.foldr f value (unwrap array)
207 |
208 |
209 | -- | Reduce an array from the left. Read @foldl@ as fold from the left.
210 | --
211 | -- > foldl (:) [] (fromLinkedList [1,2,3]) == [3,2,1]
212 | foldl :: (a -> b -> b) -> b -> Array a -> b
213 | foldl f value array =
214 | Data.Foldable.foldl' (\a b -> f b a) value (unwrap array)
215 |
216 |
217 | -- | Keep elements that pass the test.
218 | --
219 | -- > takeIf isEven (fromLinkedList [1,2,3,4,5,6]) == (fromLinkedList [2,4,6])
220 | takeIf :: (a -> Bool) -> Array a -> Array a
221 | takeIf f (Array vector) =
222 | Array (Data.Vector.filter f vector)
223 |
224 |
225 | -- | Drop elements that pass the test.
226 | --
227 | -- > dropIf isEven (fromLinkedList [1,2,3,4,5,6]) == (fromLinkedList [1,3,5])
228 | dropIf :: (a -> Bool) -> Array a -> Array a
229 | dropIf f (Array vector) =
230 | Array (Data.Vector.filter (f .> not) vector)
231 |
232 |
233 | -- | Apply a function on every element in an array.
234 | --
235 | -- > map sqrt (fromLinkedList [1,4,9]) == fromLinkedList [1,2,3]
236 | map :: (a -> b) -> Array a -> Array b
237 | map f (Array vector) =
238 | Array (Data.Vector.map f vector)
239 |
240 |
241 | -- | Apply a function on every element with its index as first argument.
242 | --
243 | -- > indexedMap (*) (fromLinkedList [5,5,5]) == fromLinkedList [0,5,10]
244 | indexedMap :: (Int -> a -> b) -> Array a -> Array b
245 | indexedMap f (Array vector) =
246 | Array (Data.Vector.imap (Prelude.fromIntegral .> f) vector)
247 |
248 |
249 | -- | Append two arrays to a new one.
250 | --
251 | -- > append (repeat 2 42) (repeat 3 81) == fromLinkedList [42,42,81,81,81]
252 | append :: Array a -> Array a -> Array a
253 | append (Array first) (Array second) =
254 | Array (first ++ second)
255 |
256 |
257 | -- | Get a sub-section of an array: @(slice start end array)@. The @start@ is a
258 | -- zero-based index where we will start our slice. The @end@ is a zero-based index
259 | -- that indicates the end of the slice. The slice extracts up to but not including
260 | -- @end@.
261 | --
262 | -- > slice 0 3 (fromLinkedList [0,1,2,3,4]) == fromLinkedList [0,1,2]
263 | -- > slice 1 4 (fromLinkedList [0,1,2,3,4]) == fromLinkedList [1,2,3]
264 | --
265 | -- Both the @start@ and @end@ indexes can be negative, indicating an offset from
266 | -- the end of the array.
267 | --
268 | -- > slice 1 (-1) (fromLinkedList [0,1,2,3,4]) == fromLinkedList [1,2,3]
269 | -- > slice (-2) 5 (fromLinkedList [0,1,2,3,4]) == fromLinkedList [3,4]
270 | --
271 | -- This makes it pretty easy to @pop@ the last element off of an array:
272 | -- @slice 0 -1 array@
273 | slice :: Int -> Int -> Array a -> Array a
274 | slice from to (Array vector)
275 | | sliceLen <= 0 = empty
276 | | otherwise = Array <| Data.Vector.slice from' sliceLen vector
277 | where
278 | len = Data.Vector.length vector
279 | handleNegative value
280 | | value < 0 = len + value
281 | | otherwise = value
282 | normalize =
283 | Prelude.fromIntegral
284 | .> handleNegative
285 | .> clamp 0 len
286 | from' = normalize from
287 | to' = normalize to
288 | sliceLen = to' - from'
289 |
290 |
291 | -- | Applies a function to each element of an array and flattens the resulting arrays into a single array.
292 | --
293 | -- This function takes a function `f` and an array `array`. It applies `f` to each element of `array` and
294 | -- collects the resulting arrays into a single array. The resulting array is then returned.
295 | --
296 | -- The function `f` should take an element of type `a` and return an array of type `Array b`.
297 | --
298 | -- The `flatMap` function is implemented using the `map` and `foldr` functions. First, it applies `f` to each
299 | -- element of `array` using the `map` function. Then, it flattens the resulting arrays into a single array
300 | -- using the `foldr` function with the `append` function as the folding operation and `empty` as the initial
301 | -- value. The resulting array is then returned.
302 |
303 | -- This function is commonly used in functional programming to apply a function to each element of a nested
304 | -- data structure and flatten the resulting structure into a single level.
305 | flatMap ::
306 | -- | The function to apply to each element of the array.
307 | (a -> Array b) ->
308 | -- | The input array.
309 | Array a ->
310 | -- | The resulting flattened array.
311 | Array b
312 | flatMap f array =
313 | array
314 | |> map f
315 | |> foldr append empty
316 |
317 |
318 | -- | TODO: Find a better name for this function.
319 | foldM :: forall (a :: Type) (b :: Type). (b -> a -> IO b) -> b -> Array a -> IO b
320 | foldM f initial self =
321 | unwrap self
322 | |> Data.Foldable.foldlM f initial
323 |
324 |
325 | -- | Drop elements from the beginning of an array until the given predicate
326 | -- returns false.
327 | dropWhile :: forall (value :: Type). (value -> Bool) -> Array value -> Array value
328 | dropWhile predicate (Array vector) = Array (Data.Vector.dropWhile predicate vector)
329 |
330 |
331 | -- | Keep elements from the beginning of an array until the given predicate
332 | -- returns false.
333 | takeWhile :: forall (value :: Type). (value -> Bool) -> Array value -> Array value
334 | takeWhile predicate (Array vector) = Array (Data.Vector.takeWhile predicate vector)
335 |
336 |
337 | -- | Partition an array into two subarrays based on a predicate.
338 | -- The first array contains elements that satisfy the predicate,
339 | -- while the second contains elements that do not.
340 | --
341 | -- > partitionBy isEven (fromLinkedList [1,2,3,4,5,6]) == (fromLinkedList [2,4,6], fromLinkedList [1,3,5])
342 | partitionBy :: forall (value :: Type). (value -> Bool) -> Array value -> (Array value, Array value)
343 | partitionBy predicate (Array vector) = do
344 | let (matching, nonMatching) = Data.Vector.partition predicate vector
345 | (Array matching, Array nonMatching)
346 |
347 |
348 | -- | Split an array into its first element and the remaining elements.
349 | -- If the array is empty, return `Nothing`.
350 | --
351 | -- > splitFirst (fromLinkedList [1,2,3]) == Just (1, fromLinkedList [2,3])
352 | splitFirst :: forall (value :: Type). Array value -> Maybe (value, Array value)
353 | splitFirst (Array vector) = do
354 | case Data.Vector.uncons vector of
355 | Nothing -> Nothing
356 | Just (first, rest) -> Just (first, Array rest)
357 |
358 |
359 | -- | Checks if any element in an array satisfies a given predicate.
360 | -- Returns `True` if at least one element matches, otherwise `False`.
361 | --
362 | -- > any isEven (fromLinkedList [1,3,5,6]) == True
363 | -- > any isEven (fromLinkedList [1,3,5]) == False
364 | any :: forall (value :: Type). (value -> Bool) -> Array value -> Bool
365 | any predicate (Array vector) = Data.Vector.any predicate vector
366 |
--------------------------------------------------------------------------------
/core/core/Bytes.hs:
--------------------------------------------------------------------------------
1 | module Bytes (Bytes (..), unwrap) where
2 |
3 | import Data.ByteString qualified as ByteString
4 | import Prelude (Show)
5 |
6 |
7 | newtype Bytes = INTERNAL_CORE_BYTES_CONSTRUCTOR ByteString.ByteString
8 | deriving (Show)
9 |
10 |
11 | unwrap :: Bytes -> ByteString.ByteString
12 | unwrap (INTERNAL_CORE_BYTES_CONSTRUCTOR bytes) = bytes
13 |
--------------------------------------------------------------------------------
/core/core/Char.hs:
--------------------------------------------------------------------------------
1 | -- | Functions for working with characters. Character literals are enclosed in @\'a\'@ pair of single quotes.
2 | --
3 | -- Since we don't have a browser and we don't have a default locale, functions
4 | -- like @toLocaleUpper@ and @toLocaleUpper@ are not supported. If you need
5 | -- something like that you can check out the 'text-icu' package which provides
6 | -- functions like @toUpper :: LocaleName -> Text -> Text@.
7 | module Char (
8 | -- * Characters
9 | Char,
10 |
11 | -- * ASCII Letters
12 | isUpper,
13 | isLower,
14 | isAlpha,
15 | isAlphaNum,
16 |
17 | -- * Digits
18 | isDigit,
19 | isOctDigit,
20 | isHexDigit,
21 |
22 | -- * Conversion
23 | toUpper,
24 | toLower,
25 |
26 | -- * Unicode Code Points
27 | toCode,
28 | fromCode,
29 | ) where
30 |
31 | import Basics
32 | import Data.Char qualified
33 | import Prelude (Char)
34 | import Prelude qualified
35 |
36 |
37 | -- | Detect upper case ASCII characters.
38 | --
39 | -- > isUpper 'A' == True
40 | -- > isUpper 'B' == True
41 | -- > ...
42 | -- > isUpper 'Z' == True
43 | --
44 | -- > isUpper '0' == False
45 | -- > isUpper 'a' == False
46 | -- > isUpper '-' == False
47 | -- > isUpper 'Σ' == False
48 | isUpper :: Char -> Bool
49 | isUpper = Data.Char.isUpper
50 |
51 |
52 | -- | Detect lower case ASCII characters.
53 | --
54 | -- > isLower 'a' == True
55 | -- > isLower 'b' == True
56 | -- > ...
57 | -- > isLower 'z' == True
58 | --
59 | -- > isLower '0' == False
60 | -- > isLower 'A' == False
61 | -- > isLower '-' == False
62 | -- > isLower 'π' == False
63 | isLower :: Char -> Bool
64 | isLower = Data.Char.isLower
65 |
66 |
67 | -- | Detect upper case and lower case ASCII characters.
68 | --
69 | -- > isAlpha 'a' == True
70 | -- > isAlpha 'b' == True
71 | -- > isAlpha 'E' == True
72 | -- > isAlpha 'Y' == True
73 | --
74 | -- > isAlpha '0' == False
75 | -- > isAlpha '-' == False
76 | -- > isAlpha 'π' == False
77 | isAlpha :: Char -> Bool
78 | isAlpha = Data.Char.isAlpha
79 |
80 |
81 | -- | Detect upper case and lower case ASCII characters.
82 | --
83 | -- > isAlphaNum 'a' == True
84 | -- > isAlphaNum 'b' == True
85 | -- > isAlphaNum 'E' == True
86 | -- > isAlphaNum 'Y' == True
87 | -- > isAlphaNum '0' == True
88 | -- > isAlphaNum '7' == True
89 | --
90 | -- > isAlphaNum '-' == False
91 | -- > isAlphaNum 'π' == False
92 | isAlphaNum :: Char -> Bool
93 | isAlphaNum = Data.Char.isAlphaNum
94 |
95 |
96 | -- | Detect digits @0123456789@
97 | --
98 | -- > isDigit '0' == True
99 | -- > isDigit '1' == True
100 | -- > ...
101 | -- > isDigit '9' == True
102 | --
103 | -- > isDigit 'a' == False
104 | -- > isDigit 'b' == False
105 | -- > isDigit 'A' == False
106 | isDigit :: Char -> Bool
107 | isDigit = Data.Char.isDigit
108 |
109 |
110 | -- | Detect octal digits @01234567@
111 | --
112 | -- > isOctDigit '0' == True
113 | -- > isOctDigit '1' == True
114 | -- > ...
115 | -- > isOctDigit '7' == True
116 | --
117 | -- > isOctDigit '8' == False
118 | -- > isOctDigit 'a' == False
119 | -- > isOctDigit 'A' == False
120 | isOctDigit :: Char -> Bool
121 | isOctDigit = Data.Char.isOctDigit
122 |
123 |
124 | -- | Detect hexadecimal digits @0123456789abcdefABCDEF@
125 | isHexDigit :: Char -> Bool
126 | isHexDigit = Data.Char.isHexDigit
127 |
128 |
129 | -- | Convert to upper case.
130 | toUpper :: Char -> Char
131 | toUpper = Data.Char.toUpper
132 |
133 |
134 | -- | Convert to lower case.
135 | toLower :: Char -> Char
136 | toLower = Data.Char.toLower
137 |
138 |
139 | -- | Convert to the corresponding Unicode [code point](https://en.wikipedia.org/wiki/Code_point).
140 | --
141 | -- > toCode 'A' == 65
142 | -- > toCode 'B' == 66
143 | -- > toCode '木' == 26408
144 | -- > toCode '𝌆' == 0x1D306
145 | -- > toCode '😃' == 0x1F603
146 | toCode :: Char -> Int
147 | toCode = Prelude.fromIntegral <. Data.Char.ord
148 |
149 |
150 | -- | Convert a Unicode [code point](https://en.wikipedia.org/wiki/Code_point) to a character.
151 | --
152 | -- > fromCode 65 == 'A'
153 | -- > fromCode 66 == 'B'
154 | -- > fromCode 0x6728 == '\26408' -- '木'
155 | -- > fromCode 0x1D306 == '\119558' -- '𝌆'
156 | -- > fromCode 0x1F603 == '\128515' -- '😃'
157 | -- > fromCode (-1) == '\65533' -- '�'
158 | --
159 | -- The full range of unicode is from @0@ to @0x10FFFF@. With numbers outside that
160 | -- range, you get [the replacement character](https://en.wikipedia.org/wiki/Specials_(Unicode_block)#Replacement_character).
161 | fromCode :: Int -> Char
162 | fromCode value
163 | | 0 <= value && value <= 0x10FFFF = Data.Char.chr (Prelude.fromIntegral value)
164 | | otherwise = '\xfffd'
165 |
--------------------------------------------------------------------------------
/core/core/Console.hs:
--------------------------------------------------------------------------------
1 | module Console (log, readLine, print) where
2 |
3 | import Appendable ((++))
4 | import Basics
5 | import Data.Text.IO qualified
6 | import GHC.Stack qualified as Stack
7 | import IO (IO)
8 | import LinkedList qualified
9 | import Maybe (Maybe (..))
10 | import Maybe qualified
11 | import System.Environment qualified
12 | import Task (Task)
13 | import Task qualified
14 | import Text (Text, fromLinkedList)
15 | import ToText (toPrettyText)
16 |
17 |
18 | print :: Text -> Task _ Unit
19 | print text = Task.fromIO do
20 | Data.Text.IO.putStrLn text
21 |
22 |
23 | -- TODO: Make this use a centralized monitoring thread
24 | log :: (Stack.HasCallStack) => Text -> IO Unit
25 | log text = do
26 | -- we check that the NEOHASKELL_DEBUG environment variable is set to true
27 | -- if it isn´t we dont do anything
28 | maybeDebug <- System.Environment.lookupEnv "NEOHASKELL_DEBUG"
29 | let debug = maybeDebug |> Maybe.withDefault "false"
30 |
31 | if debug == "true"
32 | then do
33 | -- we get the location of the call
34 |
35 | let maybeLoc =
36 | Stack.callStack
37 | |> Stack.getCallStack
38 | |> LinkedList.get 0
39 |
40 | case maybeLoc of
41 | Just (_, loc) -> do
42 | let coolLoc =
43 | Text.fromLinkedList (Stack.srcLocPackage loc)
44 | ++ ":"
45 | ++ Text.fromLinkedList (Stack.srcLocFile loc)
46 | ++ ":"
47 | ++ (Stack.srcLocStartLine loc |> toPrettyText)
48 | ++ ":"
49 | ++ (Stack.srcLocStartCol loc |> toPrettyText)
50 | let message = "[" ++ (coolLoc) ++ "]: " ++ text
51 | Data.Text.IO.putStrLn message
52 | _ -> pure ()
53 | else pure ()
54 |
55 |
56 | readLine :: IO Text
57 | readLine = Data.Text.IO.getLine
58 |
--------------------------------------------------------------------------------
/core/core/Core.hs:
--------------------------------------------------------------------------------
1 | -- | # Core
2 | --
3 | -- This module is automatically imported in all the NeoHaskell files,
4 | module Core (
5 | module Reexported,
6 | ) where
7 |
8 | import Action as Reexported (Action)
9 | import Appendable as Reexported ((++))
10 | import Array as Reexported (Array)
11 | import Basics as Reexported
12 | import Char as Reexported (Char)
13 | import ConcurrentVar as Reexported (ConcurrentVar)
14 | import Console as Reexported (log, print, readLine)
15 | import Default as Reexported (Default (..), defaultValue)
16 | import Function as Reexported
17 | -- import Html as Reexported (Html, html)
18 | import IO as Reexported (IO)
19 | import LinkedList as Reexported (LinkedList)
20 | import Map as Reexported (Map)
21 | import Maybe as Reexported (Maybe (..))
22 | import Path as Reexported (Path, path)
23 | import Result as Reexported (Result (..))
24 | -- import Service.Core as Reexported (View)
25 | import Task as Reexported (Task)
26 | import Text as Reexported (Text)
27 | import ToText as Reexported (Show, ToText, toPrettyText)
28 | import Trigger as Reexported (Trigger)
29 | import Unknown as Reexported (Unknown)
30 | import Uuid as Reexported (Uuid)
31 | import Var as Reexported (Var)
32 | import Version as Reexported (Version, version)
33 |
34 |
--------------------------------------------------------------------------------
/core/core/Function.hs:
--------------------------------------------------------------------------------
1 | module Function (unchanged) where
2 |
3 |
4 | -- | This function returns its input unchanged. It is what in other languages is called the identity function.
5 | unchanged :: value -> value
6 | unchanged value = value
7 | {-# INLINE unchanged #-}
8 |
--------------------------------------------------------------------------------
/core/core/IO.hs:
--------------------------------------------------------------------------------
1 | module IO (
2 | IO,
3 | yield,
4 | dangerouslyRun,
5 | finally,
6 | exitSuccess,
7 | catchAny,
8 | onException,
9 | map,
10 | try,
11 | apply,
12 | andThen,
13 | ) where
14 |
15 | import Control.Exception qualified as GHC
16 | import GHC.IO (IO)
17 | import Result (Result (..))
18 | import System.Exit qualified as GHC
19 | import System.IO.Unsafe qualified as GHC
20 | import Prelude qualified
21 |
22 |
23 | yield :: value -> IO value
24 | yield = Prelude.pure
25 |
26 |
27 | map :: (a -> b) -> IO a -> IO b
28 | map = Prelude.fmap
29 |
30 |
31 | apply :: IO (a -> b) -> IO a -> IO b
32 | apply ioFunction self =
33 | ioFunction Prelude.<*> self
34 |
35 |
36 | andThen :: (a -> IO b) -> IO a -> IO b
37 | andThen f self = do
38 | a <- self
39 | f a
40 |
41 |
42 | -- |
43 | -- `dangerouslyRun` is an alias for `unsafePerformIO`, which allows running an IO action and extracting its result as a pure value. However, it comes with significant risks and should be used with extreme caution, especially for those new to Haskell. Here's why:
44 | --
45 | -- 1. __Unpredictable Execution Timing__: When you use `dangerouslyRun`, you're forcing an IO action to be run in a specific place. But the exact time it runs is hard to predict. This can lead to surprising behavior, like reading a file before it's written.
46 | --
47 | -- 2. __Potential for Race Conditions__: If you use `dangerouslyRun` in multiple threads, you might accidentally access the same mutable state (like global variables) at the same time. This can cause race conditions, where the threads interfere with each other and cause incorrect results.
48 | --
49 | -- 3. __Difficulty Reasoning about Code__: In other languages, you're used to side effects happening in a specific order. But with `dangerouslyRun`, side effects can happen in unexpected places. This makes it much harder to understand how the code works, both for you and others reading your code.
50 | --
51 | -- 4. __Hard to Test and Maintain__: Code using `dangerouslyRun` is harder to test because the results can change depending on things outside the function. If you want to change the code later, you might have to change all the places it's used, making maintenance harder.
52 | --
53 | -- In general, it's best to avoid `dangerouslyRun`.
54 | dangerouslyRun :: IO a -> a
55 | dangerouslyRun = GHC.unsafePerformIO
56 |
57 |
58 | finally :: IO a -> IO b -> IO a
59 | finally action cleanup = GHC.finally action cleanup
60 |
61 |
62 | onException :: IO a -> IO b -> IO a
63 | onException action cleanup = GHC.onException action cleanup
64 |
65 |
66 | exitSuccess :: IO a
67 | exitSuccess = GHC.exitSuccess
68 |
69 |
70 | catchAny :: (GHC.SomeException -> IO a) -> IO a -> IO a
71 | catchAny handler action = GHC.catch action handler
72 |
73 |
74 | try :: IO a -> IO (Result GHC.SomeException a)
75 | try action = do
76 | res <- GHC.try action
77 | case res of
78 | Prelude.Left e -> Prelude.pure (Err e)
79 | Prelude.Right a -> Prelude.pure (Ok a)
80 |
--------------------------------------------------------------------------------
/core/core/Int.hs:
--------------------------------------------------------------------------------
1 | module Int (
2 | toFloat,
3 | ) where
4 |
5 | import Basics
6 | import Prelude qualified
7 |
8 |
9 | -- * Int to Float / Float to Int
10 |
11 |
12 | -- | Convert an integer into a float. Useful when mixing @Int@ and @Float@
13 | -- values like this:
14 | --
15 | -- > halfOf :: Int -> Float
16 | -- > halfOf number =
17 | -- > toFloat number / 2
18 | toFloat :: Int -> Float
19 | toFloat =
20 | Prelude.fromIntegral
21 |
--------------------------------------------------------------------------------
/core/core/LinkedList.hs:
--------------------------------------------------------------------------------
1 | -- | You can create a @LinkedList@ in Elm with the @[1,2,3]@ syntax, so lists are used all over the place. This module has a bunch of functions to help you work with them!
2 | module LinkedList (
3 | LinkedList,
4 |
5 | -- * Create
6 | singleton,
7 | repeat,
8 | range,
9 |
10 | -- * Transform
11 | map,
12 | indexedMap,
13 | foldl,
14 | foldr,
15 | filter,
16 | filterMap,
17 |
18 | -- * Utilities
19 | length,
20 | reverse,
21 | member,
22 | all,
23 | any,
24 | maximum,
25 | minimum,
26 | sum,
27 | product,
28 |
29 | -- * Combine
30 | append,
31 | concat,
32 | concatMap,
33 | intersperse,
34 | map2,
35 | map3,
36 | map4,
37 | map5,
38 |
39 | -- * Sort
40 | sort,
41 | sortBy,
42 | sortWith,
43 |
44 | -- * Deconstruct
45 | isEmpty,
46 | head,
47 | tail,
48 | take,
49 | drop,
50 | partition,
51 | unzip,
52 | get,
53 | ) where
54 |
55 | import Basics
56 | import Data.Foldable qualified
57 | import Data.List qualified
58 | import Data.Maybe qualified
59 | import Mappable qualified
60 | import Maybe (Maybe (..))
61 | import Thenable qualified
62 | import Prelude qualified
63 |
64 |
65 | -- | In Haskell a list type is defined using square brackets. This alias allows
66 | -- us to alternatively write the type like we would in Elm.
67 | type LinkedList a = [a]
68 |
69 |
70 | -- CREATE
71 |
72 | -- | Create a list with only one element:
73 | --
74 | -- > singleton 1234 == [1234]
75 | -- > singleton "hi" == ["hi"]
76 | singleton :: a -> LinkedList a
77 | singleton value = [value]
78 |
79 |
80 | -- | Create a list with *n* copies of a value:
81 | --
82 | -- > repeat 3 (0,0) == [(0,0),(0,0),(0,0)]
83 | repeat :: Int -> a -> LinkedList a
84 | repeat =
85 | Prelude.fromIntegral .> Data.List.replicate
86 |
87 |
88 | -- | Create a list of numbers, every element increasing by one.
89 | --
90 | -- You give the lowest and highest number that should be in the list.
91 | --
92 | -- > range 3 6 == [3, 4, 5, 6]
93 | -- > range 3 3 == [3]
94 | -- > range 6 3 == []
95 | range :: Int -> Int -> LinkedList Int
96 | range lo hi =
97 | [lo .. hi]
98 |
99 |
100 | -- TRANSFORM
101 |
102 | -- | Apply a function to every element of a list.
103 | --
104 | -- > map sqrt [1,4,9] == [1,2,3]
105 | -- > map not [True,False,True] == [False,True,False]
106 | --
107 | -- So @map func [ a, b, c ]@ is the same as @[ func a, func b, func c ]@
108 | map :: (a -> b) -> LinkedList a -> LinkedList b
109 | map =
110 | Mappable.map
111 |
112 |
113 | -- | Same as @map@ but the function is also applied to the index of each element
114 | -- (starting at zero).
115 | --
116 | -- > indexedMap Tuple.pair ["Tom","Sue","Bob"] == [ (0,"Tom"), (1,"Sue"), (2,"Bob") ]
117 | indexedMap :: (Int -> a -> b) -> LinkedList a -> LinkedList b
118 | indexedMap f xs =
119 | map2 f [0 .. (length xs - 1)] xs
120 |
121 |
122 | -- | Reduce a list from the left.
123 | --
124 | -- > foldl (+) 0 [1,2,3] == 6
125 | -- > foldl (::) [] [1,2,3] == [3,2,1]
126 | --
127 | -- So @foldl step state [1,2,3]@ is like saying:
128 | --
129 | -- > state
130 | -- > |> step 1
131 | -- > |> step 2
132 | -- > |> step 3
133 | --
134 | -- Note: This function is implemented using fold' to eagerly evaluate the
135 | -- accumulator, preventing space leaks.
136 | foldl :: (a -> b -> b) -> b -> LinkedList a -> b
137 | foldl func =
138 | Data.List.foldl' (\a b -> func b a)
139 |
140 |
141 | -- | Reduce a list from the right.
142 | --
143 | -- > foldr (+) 0 [1,2,3] == 6
144 | -- > foldr (::) [] [1,2,3] == [1,2,3]
145 | --
146 | -- So @foldr step state [1,2,3]@ is like saying:
147 | --
148 | -- > state
149 | -- > |> step 3
150 | -- > |> step 2
151 | -- > |> step 1
152 | foldr :: (a -> b -> b) -> b -> LinkedList a -> b
153 | foldr =
154 | Data.List.foldr
155 |
156 |
157 | -- | Keep elements that satisfy the test.
158 | --
159 | -- > filter isEven [1,2,3,4,5,6] == [2,4,6]
160 | filter :: (a -> Bool) -> LinkedList a -> LinkedList a
161 | filter =
162 | Data.List.filter
163 |
164 |
165 | -- | Filter out certain values. For example, maybe you have a bunch of strings
166 | -- from an untrusted source and you want to turn them into numbers:
167 | --
168 | -- > numbers : LinkedList Int
169 | -- > numbers =
170 | -- > filterMap String.toInt ["3", "hi", "12", "4th", "May"]
171 | -- > -- numbers == [3, 12]
172 | filterMap :: (a -> Maybe b) -> LinkedList a -> LinkedList b
173 | filterMap =
174 | Data.Maybe.mapMaybe
175 |
176 |
177 | -- UTILITIES
178 |
179 | -- | Determine the length of a list.
180 | --
181 | -- > length [1,2,3] == 3
182 | length :: LinkedList a -> Int
183 | length =
184 | Data.List.length .> Prelude.fromIntegral
185 |
186 |
187 | -- | Reverse a list.
188 | -- > reverse [1,2,3,4] == [4,3,2,1]
189 | reverse :: LinkedList a -> LinkedList a
190 | reverse =
191 | Data.List.reverse
192 |
193 |
194 | -- | Figure out whether a list contains a value.
195 | --
196 | -- > member 9 [1,2,3,4] == False
197 | -- > member 4 [1,2,3,4] == True
198 | member :: (Prelude.Eq a) => a -> LinkedList a -> Bool
199 | member =
200 | Data.List.elem
201 |
202 |
203 | -- | Determine if all elements satisfy some test.
204 | --
205 | -- > all isEven [2,4] == True
206 | -- > all isEven [2,3] == False
207 | -- > all isEven [] == True
208 | all :: (a -> Bool) -> LinkedList a -> Bool
209 | all =
210 | Data.List.all
211 |
212 |
213 | -- | Determine if any elements satisfy some test.
214 | --
215 | -- > any isEven [2,3] == True
216 | -- > any isEven [1,3] == False
217 | -- > any isEven [] == False
218 | any :: (a -> Bool) -> LinkedList a -> Bool
219 | any =
220 | Data.List.any
221 |
222 |
223 | -- | Find the maximum element in a non-empty list.
224 | --
225 | -- > maximum [1,4,2] == Just 4
226 | -- > maximum [] == Nothing
227 | maximum :: (Ord a) => LinkedList a -> Maybe a
228 | maximum list =
229 | case list of
230 | [] ->
231 | Nothing
232 | _ ->
233 | Just (Data.List.maximum list)
234 |
235 |
236 | -- | Find the minimum element in a non-empty list.
237 | --
238 | -- > minimum [3,2,1] == Just 1
239 | -- > minimum [] == Nothing
240 | minimum :: (Ord a) => LinkedList a -> Maybe a
241 | minimum list =
242 | case list of
243 | [] ->
244 | Nothing
245 | _ ->
246 | Just (Data.List.minimum list)
247 |
248 |
249 | -- | Get the sum of the list elements.
250 | --
251 | -- > sum [1,2,3,4] == 10
252 | sum :: (Num a) => LinkedList a -> a
253 | sum =
254 | Data.Foldable.sum
255 |
256 |
257 | -- | Get the product of the list elements.
258 | --
259 | -- > product [1,2,3,4] == 24
260 | product :: (Num a) => LinkedList a -> a
261 | product =
262 | Data.Foldable.product
263 |
264 |
265 | -- COMBINE
266 |
267 | -- | Put two lists together.
268 | --
269 | -- > append [1,1,2] [3,5,8] == [1,1,2,3,5,8]
270 | -- > append ['a','b'] ['c'] == ['a','b','c']
271 | --
272 | -- You can also use the @(++)@ operator to append lists.
273 | append :: LinkedList a -> LinkedList a -> LinkedList a
274 | append =
275 | Prelude.mappend
276 |
277 |
278 | -- | Concatenate a bunch of lists into a single list:
279 | --
280 | -- > concat [[1,2],[3],[4,5]] == [1,2,3,4,5]
281 | concat :: LinkedList (LinkedList a) -> LinkedList a
282 | concat =
283 | Prelude.mconcat
284 |
285 |
286 | -- | Map a given function onto a list and flatten the resulting lists.
287 | --
288 | -- > concatMap f xs == concat (map f xs)
289 | concatMap :: (a -> LinkedList b) -> LinkedList a -> LinkedList b
290 | concatMap =
291 | Thenable.andThen
292 |
293 |
294 | -- | Places the given value between all members of the given list.
295 | --
296 | -- > intersperse "on" ["turtles","turtles","turtles"] == ["turtles","on","turtles","on","turtles"]
297 | intersperse :: a -> LinkedList a -> LinkedList a
298 | intersperse =
299 | Data.List.intersperse
300 |
301 |
302 | -- | Combine two lists, combining them with the given function.
303 | -- If one list is longer, the extra elements are dropped.
304 | --
305 | -- > totals : LinkedList Int -> LinkedList Int -> LinkedList Int
306 | -- > totals xs ys =
307 | -- > LinkedList.map2 (+) xs ys
308 | -- >
309 | -- > -- totals [1,2,3] [4,5,6] == [5,7,9]
310 | -- >
311 | -- > pairs : LinkedList a -> LinkedList b -> LinkedList (a,b)
312 | -- > pairs xs ys =
313 | -- > LinkedList.map2 Tuple.pair xs ys
314 | -- >
315 | -- > -- pairs ["alice","bob","chuck"] [2,5,7,8]
316 | -- > -- == [("alice",2),("bob",5),("chuck",7)]
317 | --
318 | -- __Note:__ This behaves differently than 'NriPrelude.map2', which produces
319 | -- all combinations of elements from both lists.
320 | map2 :: (a -> b -> result) -> LinkedList a -> LinkedList b -> LinkedList result
321 | map2 =
322 | Data.List.zipWith
323 |
324 |
325 | -- | __Note:__ This behaves differently than 'NriPrelude.map3', which produces
326 | -- all combinations of elements from all lists.
327 | map3 :: (a -> b -> c -> result) -> LinkedList a -> LinkedList b -> LinkedList c -> LinkedList result
328 | map3 =
329 | Data.List.zipWith3
330 |
331 |
332 | -- | __Note:__ This behaves differently than 'NriPrelude.map4', which produces
333 | -- all combinations of elements from all lists.
334 | map4 ::
335 | (a -> b -> c -> d -> result) -> LinkedList a -> LinkedList b -> LinkedList c -> LinkedList d -> LinkedList result
336 | map4 =
337 | Data.List.zipWith4
338 |
339 |
340 | -- | __Note:__ This behaves differently than 'NriPrelude.map5', which produces
341 | -- all combinations of elements from all lists.
342 | map5 ::
343 | (a -> b -> c -> d -> e -> result) ->
344 | LinkedList a ->
345 | LinkedList b ->
346 | LinkedList c ->
347 | LinkedList d ->
348 | LinkedList e ->
349 | LinkedList result
350 | map5 =
351 | Data.List.zipWith5
352 |
353 |
354 | -- SORT
355 |
356 | -- | Sort values from lowest to highest
357 | --
358 | -- > sort [3,1,5] == [1,3,5]
359 | sort :: (Ord a) => LinkedList a -> LinkedList a
360 | sort =
361 | Data.List.sort
362 |
363 |
364 | -- | Sort values by a derived property.
365 | --
366 | -- > alice = { name="Alice", height=1.62 }
367 | -- > bob = { name="Bob" , height=1.85 }
368 | -- > chuck = { name="Chuck", height=1.76 }
369 | -- >
370 | -- > sortBy .name [chuck,alice,bob] == [alice,bob,chuck]
371 | -- > sortBy .height [chuck,alice,bob] == [alice,chuck,bob]
372 | -- >
373 | -- > sortBy String.length ["mouse","cat"] == ["cat","mouse"]
374 | sortBy :: (Ord b) => (a -> b) -> LinkedList a -> LinkedList a
375 | sortBy =
376 | Data.List.sortOn
377 |
378 |
379 | -- | Sort values with a custom comparison function.
380 | --
381 | -- > sortWith flippedComparison [1,2,3,4,5] == [5,4,3,2,1]
382 | -- > flippedComparison a b =
383 | -- > case compare a b of
384 | -- > LT -> GT
385 | -- > EQ -> EQ
386 | -- > GT -> LT
387 | --
388 | -- This is also the most general sort function, allowing you
389 | -- to define any other: @sort == sortWith compare@
390 | sortWith :: (a -> a -> Ordering) -> LinkedList a -> LinkedList a
391 | sortWith =
392 | Data.List.sortBy
393 |
394 |
395 | -- DECONSTRUCT
396 |
397 | -- | Determine if a list is empty.
398 | --
399 | -- > isEmpty [] == True
400 | --
401 | -- __Note:__ It is usually preferable to use a @case@ to test this so you do not
402 | -- forget to handle the @(x :: xs)@ case as well!
403 | isEmpty :: LinkedList a -> Bool
404 | isEmpty =
405 | Data.List.null
406 |
407 |
408 | -- | Extract the first element of a list.
409 | --
410 | -- > head [1,2,3] == Just 1
411 | -- > head [] == Nothing
412 | --
413 | -- __Note:__ It is usually preferable to use a @case@ to deconstruct a @LinkedList@
414 | -- because it gives you @(x :: xs)@ and you can work with both subparts.
415 | head :: LinkedList a -> Maybe a
416 | head xs =
417 | case xs of
418 | x : _ ->
419 | Just x
420 | _ ->
421 | Nothing
422 |
423 |
424 | -- | Extract the rest of the list.
425 | --
426 | -- > tail [1,2,3] == Just [2,3]
427 | -- > tail [] == Nothing
428 | --
429 | -- __Note:__ It is usually preferable to use a @case@ to deconstruct a @LinkedList@
430 | -- because it gives you @(x :: xs)@ and you can work with both subparts.
431 | tail :: LinkedList a -> Maybe (LinkedList a)
432 | tail list =
433 | case list of
434 | _ : xs ->
435 | Just xs
436 | _ ->
437 | Nothing
438 |
439 |
440 | -- | Take the first *n* members of a list.
441 | --
442 | -- > take 2 [1,2,3,4] == [1,2]
443 | take :: Int -> LinkedList a -> LinkedList a
444 | take =
445 | Prelude.fromIntegral .> Data.List.take
446 |
447 |
448 | -- | Drop the first *n* members of a list.
449 | --
450 | -- > drop 2 [1,2,3,4] == [3,4]
451 | drop :: Int -> LinkedList a -> LinkedList a
452 | drop =
453 | Prelude.fromIntegral .> Data.List.drop
454 |
455 |
456 | -- | Partition a list based on some test. The first list contains all values
457 | -- that satisfy the test, and the second list contains all the value that do not.
458 | --
459 | -- > partition (\x -> x < 3) [0,1,2,3,4,5] == ([0,1,2], [3,4,5])
460 | -- > partition isEven [0,1,2,3,4,5] == ([0,2,4], [1,3,5])
461 | partition :: (a -> Bool) -> LinkedList a -> (LinkedList a, LinkedList a)
462 | partition =
463 | Data.List.partition
464 |
465 |
466 | -- | Decompose a list of tuples into a tuple of lists.
467 | --
468 | -- > unzip [(0, True), (17, False), (1337, True)] == ([0,17,1337], [True,False,True])
469 | unzip :: LinkedList (a, b) -> (LinkedList a, LinkedList b)
470 | unzip =
471 | Data.List.unzip
472 |
473 |
474 | -- FIXME: Use safe functions for these
475 | get :: Int -> LinkedList a -> Maybe a
476 | get index list =
477 | case drop index list of
478 | x : _ ->
479 | Just x
480 | _ ->
481 | Nothing
482 |
--------------------------------------------------------------------------------
/core/core/Map.hs:
--------------------------------------------------------------------------------
1 | module Map (
2 | Map,
3 | build,
4 | HaskellMap.empty,
5 | (-->),
6 | set,
7 | get,
8 | reduce,
9 | merge,
10 | entries,
11 | ) where
12 |
13 | import Accumulator (Accumulator)
14 | import Accumulator qualified
15 | import Array (Array)
16 | import Array qualified
17 | import Basics
18 | import Data.Map.Strict (Map)
19 | import Data.Map.Strict qualified as HaskellMap
20 | import Maybe (Maybe)
21 |
22 |
23 | -- | Merge two `Map`s.
24 | merge :: (Eq key, Ord key) => Map key value -> Map key value -> Map key value
25 | merge left right = HaskellMap.union left right
26 |
27 |
28 | -- | Accumulator operator to build a `Map`
29 | (-->) ::
30 | (Eq key, Ord key) =>
31 | key ->
32 | value ->
33 | Accumulator (Map key value)
34 | (-->) key value =
35 | HaskellMap.singleton key value
36 | |> Accumulator.push
37 |
38 |
39 | -- |
40 | -- A builder API like this can be easily implemented through the
41 | -- usage of the `Accumulator` module. `-->` is an operator that
42 | -- will push a tuple into the accumulator list. After that, the
43 | -- `build` function will convert the accumulator list into a
44 | -- `Map`.
45 | --
46 | -- Example:
47 | --
48 | -- ```haskell
49 | -- Map.build do
50 | -- "a" --> 1
51 | -- "b" --> 2
52 | -- "c" --> 3
53 | -- ```
54 | build :: Accumulator (Map key value) -> Map key value
55 | build = Accumulator.accumulate
56 |
57 |
58 | -- | Set a value in a `Map`.
59 | set :: (Eq key, Ord key) => key -> value -> Map key value -> Map key value
60 | set key value map = (HaskellMap.insert key value map)
61 |
62 |
63 | -- | Get the value from a `Map`.
64 | get :: (Eq key, Ord key) => key -> Map key value -> Maybe value
65 | get key map = HaskellMap.lookup key map
66 |
67 |
68 | -- | Reduce a `Map`.
69 | reduce :: acc -> (key -> value -> acc -> acc) -> Map key value -> acc
70 | reduce acc f map = HaskellMap.foldrWithKey f acc map
71 |
72 |
73 | -- | Converts a map to an array of tuples
74 | entries :: Map key value -> Array (key, value)
75 | entries self = HaskellMap.toList self |> Array.fromLinkedList
76 |
--------------------------------------------------------------------------------
/core/core/Maybe.hs:
--------------------------------------------------------------------------------
1 | -- | This library fills a bunch of important niches in Elm. A Maybe can help you with optional arguments, error handling, and records with optional fields.
2 | module Maybe (
3 | -- * Definition
4 | Maybe (..),
5 |
6 | -- * Common Helpers
7 | withDefault,
8 | map,
9 |
10 | -- * Chaining Maybes
11 | andThen,
12 | getOrDie,
13 | ) where
14 |
15 | import Basics
16 | import Data.Maybe (Maybe (..), fromMaybe)
17 | import Mappable qualified
18 | import Thenable qualified
19 |
20 |
21 | -- | Provide a default value, turning an optional value into a normal
22 | -- value. This comes in handy when paired with functions like
23 | -- 'Dict.get' which gives back a @Maybe@.
24 | --
25 | -- __Note:__ This can be overused! Many cases are better handled by a @case@
26 | -- expression. And if you end up using @withDefault@ a lot, it can be a good sign
27 | -- that a [custom type](https://guide.elm-lang.org/types/custom_types.html)
28 | -- will clean your code up quite a bit!
29 | withDefault :: a -> Maybe a -> a
30 | withDefault =
31 | Data.Maybe.fromMaybe
32 |
33 |
34 | -- | Transform a @Maybe@ value with a function:
35 | map :: (a -> b) -> Maybe a -> Maybe b
36 | map =
37 | Mappable.map
38 |
39 |
40 | -- | Chain together many computations that may fail. It is helpful to see an
41 | -- equivalent definition:
42 | --
43 | -- > andThen :: (a -> Maybe b) -> Maybe a -> Maybe b
44 | -- > andThen callback maybe =
45 | -- > case maybe of
46 | -- > Just value ->
47 | -- > callback value
48 | -- >
49 | -- > Nothing ->
50 | -- > Nothing
51 | --
52 | -- This means we only continue with the callback if things are going well. For
53 | -- example, say you need to parse some user input as a month:
54 | --
55 | -- > parseMonth :: String -> Maybe Int
56 | -- > parseMonth userInput =
57 | -- > String.toInt userInput
58 | -- > |> andThen toValidMonth
59 | -- >
60 | -- > toValidMonth :: Int -> Maybe Int
61 | -- > toValidMonth month =
62 | -- > if 1 <= month && month <= 12 then
63 | -- > Just month
64 | -- >
65 | -- > else
66 | -- > Nothing
67 | --
68 | -- In the @parseMonth' function, if 'String.toInt@ produces @Nothing@ (because
69 | -- the @userInput@ was not an integer) this entire chain of operations will
70 | -- short-circuit and result in @Nothing@. If @toValidMonth@ results in
71 | -- @Nothing@, again the chain of computations will result in @Nothing@.
72 | andThen :: (a -> Maybe b) -> Maybe a -> Maybe b
73 | andThen =
74 | Thenable.andThen
75 |
76 |
77 | -- | Attempts to retrieve the value from a @Maybe@. If the @Maybe@ is @Nothing@,
78 | -- the application will crash abruptly.
79 | getOrDie :: Maybe a -> a
80 | getOrDie maybe =
81 | case maybe of
82 | Just value ->
83 | value
84 | Nothing ->
85 | panic "Maybe.getOrDie: Got Nothing"
86 | {-# INLINE getOrDie #-}
87 |
--------------------------------------------------------------------------------
/core/core/Record.hs:
--------------------------------------------------------------------------------
1 | module Record (Record) where
2 |
3 |
4 | data Record a
5 |
--------------------------------------------------------------------------------
/core/core/Result.hs:
--------------------------------------------------------------------------------
1 | -- | A @Result@ is the result of a computation that may fail. This is a great
2 | -- way to manage errors in Elm, but we when using this package in Haskell we
3 | -- tend to rely on 'Task.Task' a lot too for error handling.
4 | module Result (
5 | -- * Type and Constructors
6 | Result (..),
7 |
8 | -- * Mapping
9 | map,
10 |
11 | -- * Chaining
12 | andThen,
13 |
14 | -- * Handling Errors
15 | withDefault,
16 | toMaybe,
17 | fromMaybe,
18 | mapError,
19 | fromEither,
20 | ) where
21 |
22 | import Basics
23 | import Mappable qualified
24 | import Maybe (Maybe (..))
25 | import Thenable qualified
26 | import Prelude qualified
27 |
28 |
29 | -- | A @Result@ is either @Ok@ meaning the computation succeeded, or it is an
30 | -- @Err@ meaning that there was some failure.
31 | data Result error value
32 | = Ok value
33 | | Err error
34 | deriving (Prelude.Show, Eq)
35 |
36 |
37 | instance Prelude.Functor (Result error) where
38 | fmap func result =
39 | case result of
40 | Ok value -> Ok (func value)
41 | Err error -> Err error
42 |
43 |
44 | instance Prelude.Applicative (Result error) where
45 | pure = Ok
46 |
47 |
48 | (<*>) r1 r2 =
49 | case (r1, r2) of
50 | (Ok func, Ok a) -> Ok (func a)
51 | (Err error, _) -> Err error
52 | (Ok _, Err error) -> Err error
53 |
54 |
55 | instance Prelude.Monad (Result error) where
56 | (>>=) result func =
57 | case result of
58 | Ok value -> func value
59 | Err error -> Err error
60 |
61 |
62 | -- | If the result is @Ok@ return the value, but if the result is an @Err@ then
63 | -- return a given default value. The following examples try to parse integers.
64 | --
65 | -- > Result.withDefault 0 (Ok 123) == 123
66 | -- > Result.withDefault 0 (Err "no") == 0
67 | withDefault :: a -> Result b a -> a
68 | withDefault fallback result =
69 | case result of
70 | Ok value -> value
71 | Err _ -> fallback
72 |
73 |
74 | -- | Apply a function to a result. If the result is @Ok@, it will be converted.
75 | -- If the result is an @Err@, the same error value will propagate through.
76 | --
77 | -- > map sqrt (Ok 4.0) == Ok 2.0
78 | -- > map sqrt (Err "bad input") == Err "bad input"
79 | map :: (a -> value) -> Result x a -> Result x value
80 | map =
81 | Mappable.map
82 |
83 |
84 | -- | Chain together a sequence of computations that may fail. It is helpful
85 | -- to see its definition:
86 | --
87 | -- > andThen : (a -> Result e b) -> Result e a -> Result e b
88 | -- > andThen callback result =
89 | -- > case result of
90 | -- > Ok value -> callback value
91 | -- > Err msg -> Err msg
92 | --
93 | -- This means we only continue with the callback if things are going well. For
94 | -- example, say you need to use (@toInt : String -> Result String Int@) to parse
95 | -- a month and make sure it is between 1 and 12:
96 | --
97 | -- > toValidMonth : Int -> Result String Int
98 | -- > toValidMonth month =
99 | -- > if month >= 1 && month <= 12
100 | -- > then Ok month
101 | -- > else Err "months must be between 1 and 12"
102 | --
103 | -- > toMonth : String -> Result String Int
104 | -- > toMonth rawString =
105 | -- > toInt rawString
106 | -- > |> andThen toValidMonth
107 | --
108 | -- > -- toMonth "4" == Ok 4
109 | -- > -- toMonth "9" == Ok 9
110 | -- > -- toMonth "a" == Err "cannot parse to an Int"
111 | -- > -- toMonth "0" == Err "months must be between 1 and 12"
112 | --
113 | -- This allows us to come out of a chain of operations with quite a specific error
114 | -- message. It is often best to create a custom type that explicitly represents
115 | -- the exact ways your computation may fail. This way it is easy to handle in your
116 | -- code.
117 | andThen :: (a -> Result c b) -> Result c a -> Result c b
118 | andThen =
119 | Thenable.andThen
120 |
121 |
122 | -- | Transform an @Err@ value. For example, say the errors we get have too much
123 | -- information:
124 | --
125 | -- > parseInt : String -> Result ParseError Int
126 | -- >
127 | -- > type alias ParseError =
128 | -- > { message : String
129 | -- > , code : Int
130 | -- > , position : (Int,Int)
131 | -- > }
132 | -- >
133 | -- > mapError .message (parseInt "123") == Ok 123
134 | -- > mapError .message (parseInt "abc") == Err "char 'a' is not a number"
135 | mapError :: (a -> b) -> Result a c -> Result b c
136 | mapError func result =
137 | case result of
138 | Ok value -> Ok value
139 | Err error -> Err (func error)
140 |
141 |
142 | -- | Convert to a simpler @Maybe@ if the actual error message is not needed or
143 | -- you need to interact with some code that primarily uses maybes.
144 | --
145 | -- > parseInt : String -> Result ParseError Int
146 | -- >
147 | -- > maybeParseInt : String -> Maybe Int
148 | -- > maybeParseInt string =
149 | -- > toMaybe (parseInt string)
150 | toMaybe :: Result a b -> Maybe b
151 | toMaybe result =
152 | case result of
153 | Ok value -> Just value
154 | Err _ -> Nothing
155 |
156 |
157 | -- | Convert from a simple @Maybe@ to interact with some code that primarily
158 | -- uses @Results@.
159 | --
160 | -- > parseInt : String -> Maybe Int
161 | -- >
162 | -- > resultParseInt : String -> Result String Int
163 | -- > resultParseInt string =
164 | -- > fromMaybe ("error parsing string: " ++ toString string) (parseInt string)
165 | fromMaybe :: a -> Maybe b -> Result a b
166 | fromMaybe error maybe =
167 | case maybe of
168 | Just something -> Ok something
169 | Nothing -> Err error
170 |
171 |
172 | -- | Compatibility function to integrate with Haskell's @Either@ type.
173 | --
174 | -- > fromEither (Prelude.Right 42) == Ok 42
175 | -- > fromEither (Prelude.Left "nope") == Err "nope"
176 | fromEither :: Prelude.Either a b -> Result a b
177 | fromEither either =
178 | case either of
179 | Prelude.Left a -> Err a
180 | Prelude.Right b -> Ok b
181 |
--------------------------------------------------------------------------------
/core/core/Task.hs:
--------------------------------------------------------------------------------
1 | module Task (
2 | Task (..),
3 | yield,
4 | throw,
5 | map,
6 | apply,
7 | andThen,
8 | run,
9 | runOrPanic,
10 | mapError,
11 | fromFailableIO,
12 | fromIO,
13 | runMain,
14 | forEach,
15 | ) where
16 |
17 | import Applicable (Applicative (pure))
18 | import Applicable qualified
19 | import Array (Array)
20 | import Array qualified
21 | import Basics
22 | import Control.Exception (Exception)
23 | import Control.Exception qualified as Exception
24 | import Control.Monad.IO.Class qualified as Monad
25 | import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE, withExceptT)
26 | import Data.Either qualified as Either
27 | import Data.Foldable qualified
28 | import Data.Text.IO qualified as GHCText
29 | import IO (IO)
30 | import IO qualified
31 | import Main.Utf8 (withUtf8)
32 | import Mappable (Functor)
33 | import Mappable qualified
34 | import Result (Result)
35 | import Result qualified
36 | import Text (Text)
37 | import Thenable (Monad)
38 | import ToText (Show, toPrettyText)
39 |
40 |
41 | newtype Task err value = Task
42 | {runTask :: (ExceptT err IO) value}
43 | deriving (Functor, Applicable.Applicative, Monad)
44 |
45 |
46 | yield :: value -> Task _ value
47 | yield value = Task (Applicable.pure value)
48 |
49 |
50 | throw :: err -> Task err _
51 | throw err = Task (throwE err)
52 |
53 |
54 | map :: (input -> output) -> Task err input -> Task err output
55 | map f self = Task (Mappable.map f (runTask self))
56 |
57 |
58 | mapError :: (err1 -> err2) -> Task err1 value -> Task err2 value
59 | mapError f self =
60 | runTask self
61 | |> withExceptT f
62 | |> Task
63 |
64 |
65 | apply :: Task err (input -> output) -> Task err input -> Task err output
66 | apply taskFunction self = Task (Applicable.apply (runTask taskFunction) (runTask self))
67 |
68 |
69 | andThen :: (input -> Task err output) -> Task err input -> Task err output
70 | andThen f self = Task do
71 | value <- runTask self
72 | runTask (f value)
73 |
74 |
75 | -- TODO: Figure out the best API to ensure that the main function is just a Task that cannot fail and returns a unit
76 |
77 | run :: (Result err value -> IO value) -> Task err value -> IO value
78 | run reducer task =
79 | runTask task
80 | |> runExceptT
81 | |> IO.map Result.fromEither
82 | |> IO.andThen reducer
83 | |> withUtf8
84 |
85 |
86 | runOrPanic :: (Show err) => Task err value -> IO value
87 | runOrPanic task = do
88 | let reducer (Result.Ok value) = IO.yield value
89 | reducer (Result.Err err) = panic (toPrettyText err)
90 | task
91 | |> run reducer
92 | |> withUtf8
93 |
94 |
95 | runMain :: Task Text Unit -> IO Unit
96 | runMain task = do
97 | let reducer (Result.Ok _) = IO.yield ()
98 | reducer (Result.Err err) = GHCText.putStrLn err
99 | task
100 | |> run reducer
101 | |> withUtf8
102 |
103 |
104 | -- fromFailableIO is the reverse of run
105 | -- it receives an io, an exception catcher, and returns a task
106 | -- the task will run the io and catch exceptions
107 | -- if an exception is caught, it will be passed to the exception catcher
108 | -- if no exception is caught, the result will be passed to the success handler
109 | fromFailableIO ::
110 | forall exception result.
111 | (Exception exception) =>
112 | IO result ->
113 | Task exception result
114 | fromFailableIO io = do
115 | result <- io |> Exception.try @exception |> Monad.liftIO |> Task
116 | case result of
117 | Either.Left exception -> throw exception
118 | Either.Right value -> yield value
119 |
120 |
121 | fromIO :: IO value -> Task _ value
122 | fromIO io =
123 | io
124 | |> Monad.liftIO
125 | |> Task
126 |
127 |
128 | forEach ::
129 | forall (element :: Type) (err :: Type).
130 | (element -> Task err Unit) ->
131 | Array element ->
132 | Task err Unit
133 | forEach callback array =
134 | Data.Foldable.traverse_ callback (Array.unwrap array)
135 |
--------------------------------------------------------------------------------
/core/core/Text.hs:
--------------------------------------------------------------------------------
1 | -- | A built-in representation for efficient string manipulation.
2 | -- @Text@ values are /not/ lists of characters.
3 | module Text (
4 | -- * Text
5 | Text,
6 | isEmpty,
7 | length,
8 | reverse,
9 | repeat,
10 | replace,
11 |
12 | -- * Building and Splitting
13 | append,
14 | concat,
15 | split,
16 | joinWith,
17 | words,
18 | lines,
19 |
20 | -- * Get Substrings
21 | slice,
22 | left,
23 | right,
24 | dropLeft,
25 | dropRight,
26 |
27 | -- * Check for Substrings
28 | contains,
29 | startsWith,
30 | endsWith,
31 | indexes,
32 | indices,
33 |
34 | -- * Int Conversions
35 | toInt,
36 | fromInt,
37 |
38 | -- * Float Conversions
39 | toFloat,
40 | fromFloat,
41 |
42 | -- * Char Conversions
43 | fromChar,
44 | cons,
45 | uncons,
46 |
47 | -- * Array Conversions
48 | toArray,
49 | fromArray,
50 |
51 | -- * LinkedList Conversions
52 | toLinkedList,
53 | fromLinkedList,
54 |
55 | -- * Formatting
56 |
57 | -- | Cosmetic operations such as padding with extra characters or trimming whitespace.
58 | toUpper,
59 | toLower,
60 | pad,
61 | padLeft,
62 | padRight,
63 | trim,
64 | trimLeft,
65 | trimRight,
66 |
67 | -- * Higher-Order Functions
68 | map,
69 | filter,
70 | foldl,
71 | foldr,
72 | any,
73 | all,
74 |
75 | -- * Bytes Conversions
76 | toBytes,
77 | fromBytes,
78 | convert,
79 |
80 | -- * Casing Conversions
81 | toPascalCase,
82 | toCamelCase,
83 | toTitleCase,
84 | toSnakeCase,
85 | toKebabCase,
86 | ) where
87 |
88 | import Array (Array)
89 | import Array qualified
90 | import Basics
91 | import Bytes (Bytes)
92 | import Bytes qualified
93 | import Char (Char)
94 | import Data.Text qualified
95 | import Data.Text.Encoding qualified
96 | import Data.Text.Manipulate qualified as Manipulate
97 | import LinkedList (LinkedList)
98 | import Maybe (Maybe)
99 | import Text.Read qualified
100 | import Prelude qualified
101 |
102 |
103 | -- | A @Text@ is a chunk of text:
104 | --
105 | -- > "Hello!"
106 | -- > "How are you?"
107 | -- > "🙈🙉🙊"
108 | -- >
109 | -- > -- strings with escape characters
110 | -- > "this\n\t\"that\""
111 | -- > "\x1F648\x1F649\x1F64A" -- "🙈🙉🙊"
112 | --
113 | -- A @Text@ can represent any sequence of [unicode characters](https://en.wikipedia.org/wiki/Unicode). You can use the unicode escapes from @\x0000@ to @\x10FFFF@ to represent characters by their code point. You can also include the unicode characters directly. Using the escapes can be better if you need one of the many whitespace characters with different widths.
114 | type Text = Data.Text.Text
115 |
116 |
117 | -- | Determine if a string is empty.
118 | --
119 | -- > isEmpty "" == True
120 | -- > isEmpty "the world" == False
121 | isEmpty :: Text -> Bool
122 | isEmpty = Data.Text.null
123 |
124 |
125 | -- | Get the length of a string.
126 | --
127 | -- > length "innumerable" == 11
128 | -- > length "" == 0
129 | length :: Text -> Int
130 | length =
131 | Data.Text.length .> Prelude.fromIntegral
132 |
133 |
134 | -- | Reverse a string.
135 | --
136 | -- > reverse "stressed" == "desserts"
137 | reverse :: Text -> Text
138 | reverse = Data.Text.reverse
139 |
140 |
141 | -- | Repeat a string /n/ times.
142 | --
143 | -- > repeat 3 "ha" == "hahaha"
144 | repeat :: Int -> Text -> Text
145 | repeat =
146 | Prelude.fromIntegral .> Data.Text.replicate
147 |
148 |
149 | -- | Replace all occurrences of some substring.
150 | --
151 | -- > replace "." "-" "Json.Decode.succeed" == "Json-Decode-succeed"
152 | -- > replace "," "/" "a,b,c,d,e" == "a/b/c/d/e"
153 | replace :: Text -> Text -> Text -> Text
154 | replace = Data.Text.replace
155 |
156 |
157 | -- BUILDING AND SPLITTING
158 |
159 | -- | Append two strings. You can also use the @(++)@ operator to do this.
160 | --
161 | -- > append "butter" "fly" == "butterfly"
162 | append :: Text -> Text -> Text
163 | append = Data.Text.append
164 |
165 |
166 | -- | Concatenate many strings into one.
167 | --
168 | -- > concat ["never","the","less"] == "nevertheless"
169 | concat :: Array Text -> Text
170 | concat arr =
171 | Array.toLinkedList arr
172 | |> Data.Text.concat
173 |
174 |
175 | -- | Split a string using a given separator.
176 | --
177 | -- > split "," "cat,dog,cow" == ["cat","dog","cow"]
178 | -- > split "/" "home/evan/Desktop/" == ["home","evan","Desktop", ""]
179 | split :: Text -> Text -> Array Text
180 | split sep txt =
181 | Data.Text.splitOn sep txt
182 | |> Array.fromLinkedList
183 |
184 |
185 | -- | Put many strings together with a given separator.
186 | --
187 | -- > join "a" ["H","w","ii","n"] == "Hawaiian"
188 | -- > join " " ["cat","dog","cow"] == "cat dog cow"
189 | -- > join "/" ["home","evan","Desktop"] == "home/evan/Desktop"
190 | joinWith :: Text -> Array Text -> Text
191 | joinWith txt arr =
192 | Array.toLinkedList arr
193 | |> Data.Text.intercalate txt
194 |
195 |
196 | -- | Break a string into words, splitting on chunks of whitespace.
197 | --
198 | -- > words "How are \t you? \n Good?" == ["How","are","you?","Good?"]
199 | words :: Text -> Array Text
200 | words txt =
201 | Data.Text.words txt
202 | |> Array.fromLinkedList
203 |
204 |
205 | -- | Break a string into lines, splitting on newlines.
206 | --
207 | -- > lines "How are you?\nGood?" == ["How are you?", "Good?"]
208 | lines :: Text -> Array Text
209 | lines txt =
210 | Data.Text.lines txt
211 | |> Array.fromLinkedList
212 |
213 |
214 | -- SUBSTRINGS
215 |
216 | -- | Take a substring given a start and end index. Negative indexes
217 | -- are taken starting from the /end/ of the list.
218 | --
219 | -- > slice 7 9 "snakes on a plane!" == "on"
220 | -- > slice 0 6 "snakes on a plane!" == "snakes"
221 | -- > slice 0 -7 "snakes on a plane!" == "snakes on a"
222 | -- > slice -6 -1 "snakes on a plane!" == "plane"
223 | slice :: Int -> Int -> Text -> Text
224 | slice from to text
225 | | to' - from' <= 0 = Data.Text.empty
226 | | otherwise =
227 | Data.Text.drop from' (Data.Text.take to' text)
228 | where
229 | len = Data.Text.length text
230 | handleNegative value
231 | | value < 0 = len + value
232 | | otherwise = value
233 | normalize =
234 | Prelude.fromIntegral
235 | .> handleNegative
236 | .> clamp 0 len
237 | from' = normalize from
238 | to' = normalize to
239 |
240 |
241 | -- | Take /n/ characters from the left side of a string.
242 | --
243 | -- > left 2 "Mulder" == "Mu"
244 | left :: Int -> Text -> Text
245 | left =
246 | Prelude.fromIntegral .> Data.Text.take
247 |
248 |
249 | -- | Take /n/ characters from the right side of a string.
250 | --
251 | -- > right 2 "Scully" == "ly"
252 | right :: Int -> Text -> Text
253 | right =
254 | Prelude.fromIntegral .> Data.Text.takeEnd
255 |
256 |
257 | -- | Drop /n/ characters from the left side of a string.
258 | --
259 | -- > dropLeft 2 "The Lone Gunmen" == "e Lone Gunmen"
260 | dropLeft :: Int -> Text -> Text
261 | dropLeft =
262 | Prelude.fromIntegral .> Data.Text.drop
263 |
264 |
265 | -- | Drop /n/ characters from the right side of a string.
266 | --
267 | -- > dropRight 2 "Cigarette Smoking Man" == "Cigarette Smoking M"
268 | dropRight :: Int -> Text -> Text
269 | dropRight =
270 | Prelude.fromIntegral .> Data.Text.dropEnd
271 |
272 |
273 | -- DETECT SUBSTRINGS
274 |
275 | -- | See if the second string contains the first one.
276 | --
277 | -- > contains "the" "theory" == True
278 | -- > contains "hat" "theory" == False
279 | -- > contains "THE" "theory" == False
280 | contains :: Text -> Text -> Bool
281 | contains = Data.Text.isInfixOf
282 |
283 |
284 | -- | See if the second string starts with the first one.
285 | --
286 | -- > startsWith "the" "theory" == True
287 | -- > startsWith "ory" "theory" == False
288 | startsWith :: Text -> Text -> Bool
289 | startsWith = Data.Text.isPrefixOf
290 |
291 |
292 | -- | See if the second string ends with the first one.
293 | --
294 | -- > endsWith "the" "theory" == False
295 | -- > endsWith "ory" "theory" == True
296 | endsWith :: Text -> Text -> Bool
297 | endsWith = Data.Text.isSuffixOf
298 |
299 |
300 | -- | Get all of the indexes for a substring in another string.
301 | --
302 | -- > indexes "i" "Mississippi" == [1,4,7,10]
303 | -- > indexes "ss" "Mississippi" == [2,5]
304 | -- > indexes "needle" "haystack" == []
305 | indexes :: Text -> Text -> Array Int
306 | indexes n h
307 | | isEmpty n = Array.empty
308 | | otherwise = indexes' n h
309 | where
310 | indexes' needle haystack =
311 | Data.Text.breakOnAll needle haystack
312 | |> Array.fromLinkedList
313 | |> Array.map
314 | ( \(lhs, _) ->
315 | Data.Text.length lhs
316 | |> Prelude.fromIntegral
317 | )
318 |
319 |
320 | -- | Alias for @indexes@.
321 | indices :: Text -> Text -> Array Int
322 | indices = indexes
323 |
324 |
325 | -- FORMATTING
326 |
327 | -- | Convert a string to all upper case. Useful for case-insensitive comparisons
328 | -- and VIRTUAL YELLING.
329 | --
330 | -- > toUpper "skinner" == "SKINNER"
331 | toUpper :: Text -> Text
332 | toUpper = Data.Text.toUpper
333 |
334 |
335 | -- | Convert a string to all lower case. Useful for case-insensitive comparisons.
336 | --
337 | -- > toLower "X-FILES" == "x-files"
338 | toLower :: Text -> Text
339 | toLower = Data.Text.toLower
340 |
341 |
342 | -- | Pad a string on both sides until it has a given length.
343 | --
344 | -- > pad 5 ' ' "1" == " 1 "
345 | -- > pad 5 ' ' "11" == " 11 "
346 | -- > pad 5 ' ' "121" == " 121 "
347 | pad :: Int -> Char -> Text -> Text
348 | pad =
349 | Prelude.fromIntegral .> Data.Text.center
350 |
351 |
352 | -- | Pad a string on the left until it has a given length.
353 | --
354 | -- > padLeft 5 '.' "1" == "....1"
355 | -- > padLeft 5 '.' "11" == "...11"
356 | -- > padLeft 5 '.' "121" == "..121"
357 | padLeft :: Int -> Char -> Text -> Text
358 | padLeft =
359 | Prelude.fromIntegral .> Data.Text.justifyRight
360 |
361 |
362 | -- | Pad a string on the right until it has a given length.
363 | --
364 | -- > padRight 5 '.' "1" == "1...."
365 | -- > padRight 5 '.' "11" == "11..."
366 | -- > padRight 5 '.' "121" == "121.."
367 | padRight :: Int -> Char -> Text -> Text
368 | padRight =
369 | Prelude.fromIntegral .> Data.Text.justifyLeft
370 |
371 |
372 | -- | Get rid of whitespace on both sides of a string.
373 | --
374 | -- > trim " hats \n" == "hats"
375 | trim :: Text -> Text
376 | trim = Data.Text.strip
377 |
378 |
379 | -- | Get rid of whitespace on the left of a string.
380 | --
381 | -- > trimLeft " hats \n" == "hats \n"
382 | trimLeft :: Text -> Text
383 | trimLeft = Data.Text.stripStart
384 |
385 |
386 | -- | Get rid of whitespace on the right of a string.
387 | --
388 | -- > trimRight " hats \n" == " hats"
389 | trimRight :: Text -> Text
390 | trimRight = Data.Text.stripEnd
391 |
392 |
393 | -- INT CONVERSIONS
394 |
395 | -- | Try to convert a string into an int, failing on improperly formatted strings.
396 | --
397 | -- > Text.toInt "123" == Just 123
398 | -- > Text.toInt "-42" == Just -42
399 | -- > Text.toInt "3.1" == Nothing
400 | -- > Text.toInt "31a" == Nothing
401 | --
402 | -- If you are extracting a number from some raw user input, you will typically
403 | -- want to use [@Maybe.withDefault@](Maybe#withDefault) to handle bad data:
404 | --
405 | -- > Maybe.withDefault 0 (Text.toInt "42") == 42
406 | -- > Maybe.withDefault 0 (Text.toInt "ab") == 0
407 | toInt :: Text -> Maybe Int
408 | toInt text =
409 | Text.Read.readMaybe str'
410 | where
411 | str = Data.Text.unpack text
412 | str' = case str of
413 | '+' : rest -> rest
414 | other -> other
415 |
416 |
417 | -- | Convert an @Int@ to a @Text@.
418 | --
419 | -- > Text.fromInt 123 == "123"
420 | -- > Text.fromInt -42 == "-42"
421 | fromInt :: Int -> Text
422 | fromInt = Data.Text.pack <. Prelude.show
423 |
424 |
425 | -- FLOAT CONVERSIONS
426 |
427 | -- | Try to convert a string into a float, failing on improperly formatted strings.
428 | --
429 | -- > Text.toFloat "123" == Just 123.0
430 | -- > Text.toFloat "-42" == Just -42.0
431 | -- > Text.toFloat "3.1" == Just 3.1
432 | -- > Text.toFloat "31a" == Nothing
433 | --
434 | -- If you are extracting a number from some raw user input, you will typically
435 | -- want to use [@Maybe.withDefault@](Maybe#withDefault) to handle bad data:
436 | --
437 | -- > Maybe.withDefault 0 (Text.toFloat "42.5") == 42.5
438 | -- > Maybe.withDefault 0 (Text.toFloat "cats") == 0
439 | toFloat :: Text -> Maybe Float
440 | toFloat text =
441 | Text.Read.readMaybe str'
442 | where
443 | str = Data.Text.unpack text
444 | str' = case str of
445 | '+' : rest -> rest
446 | '.' : rest -> '0' : '.' : rest
447 | other -> other
448 |
449 |
450 | -- | Convert a @Float@ to a @Text@.
451 | --
452 | -- > Text.fromFloat 123 == "123"
453 | -- > Text.fromFloat -42 == "-42"
454 | -- > Text.fromFloat 3.9 == "3.9"
455 | fromFloat :: Float -> Text
456 | fromFloat = Data.Text.pack <. Prelude.show
457 |
458 |
459 | -- LIST CONVERSIONS
460 |
461 | -- | Convert a Text to a list of characters.
462 | --
463 | -- > toArray "abc" == ['a','b','c']
464 | -- > toArray "🙈🙉🙊" == ['🙈','🙉','🙊']
465 | toArray :: Text -> Array Char
466 | toArray txt =
467 | Data.Text.unpack txt
468 | |> Array.fromLinkedList
469 |
470 |
471 | -- | Convert a list of characters into a Text. Can be useful if you
472 | -- want to create a string primarily by consing, perhaps for decoding
473 | -- something.
474 | --
475 | -- > fromArray ['a','b','c'] == "abc"
476 | -- > fromArray ['🙈','🙉','🙊'] == "🙈🙉🙊"
477 | fromArray :: Array Char -> Text
478 | fromArray arr =
479 | Array.toLinkedList arr
480 | |> Data.Text.pack
481 |
482 |
483 | -- LINKED LIST CONVERSIONS
484 |
485 | -- | Convert a Text to a linked list of characters.
486 | toLinkedList :: Text -> LinkedList Char
487 | toLinkedList txt =
488 | Data.Text.unpack txt
489 |
490 |
491 | -- | Convert a linked list of characters into a Text.
492 | fromLinkedList :: LinkedList Char -> Text
493 | fromLinkedList list = Data.Text.pack list
494 |
495 |
496 | -- CHAR CONVERSIONS
497 |
498 | -- | Create a Text from a given character.
499 | --
500 | -- > fromChar 'a' == "a"
501 | fromChar :: Char -> Text
502 | fromChar = Data.Text.singleton
503 |
504 |
505 | -- | Add a character to the beginning of a Text.
506 | --
507 | -- > cons 'T' "he truth is out there" == "The truth is out there"
508 | cons :: Char -> Text -> Text
509 | cons = Data.Text.cons
510 |
511 |
512 | -- | Split a non-empty Text into its head and tail. This lets you
513 | -- pattern match on strings exactly as you would with lists.
514 | --
515 | -- > uncons "abc" == Just ('a',"bc")
516 | -- > uncons "" == Nothing
517 | uncons :: Text -> Maybe (Char, Text)
518 | uncons = Data.Text.uncons
519 |
520 |
521 | -- HIGHER-ORDER FUNCTIONS
522 |
523 | -- | Transform every character in a Text
524 | --
525 | -- > map (\c -> if c == '/' then '.' else c) "a/b/c" == "a.b.c"
526 | map :: (Char -> Char) -> Text -> Text
527 | map = Data.Text.map
528 |
529 |
530 | -- | Keep only the characters that pass the test.
531 | --
532 | -- > filter isDigit "R2-D2" == "22"
533 | filter :: (Char -> Bool) -> Text -> Text
534 | filter = Data.Text.filter
535 |
536 |
537 | -- | Reduce a Text from the left.
538 | --
539 | -- > foldl cons "" "time" == "emit"
540 | foldl :: (Char -> b -> b) -> b -> Text -> b
541 | foldl f = Data.Text.foldl' (\a b -> f b a)
542 |
543 |
544 | -- | Reduce a Text from the right.
545 | --
546 | -- > foldr cons "" "time" == "time"
547 | foldr :: (Char -> b -> b) -> b -> Text -> b
548 | foldr = Data.Text.foldr
549 |
550 |
551 | -- | Determine whether /any/ characters pass the test.
552 | --
553 | -- > any isDigit "90210" == True
554 | -- > any isDigit "R2-D2" == True
555 | -- > any isDigit "heart" == False
556 | any :: (Char -> Bool) -> Text -> Bool
557 | any = Data.Text.any
558 |
559 |
560 | -- | Determine whether /all/ characters pass the test.
561 | --
562 | -- > all isDigit "90210" == True
563 | -- > all isDigit "R2-D2" == False
564 | -- > all isDigit "heart" == False
565 | all :: (Char -> Bool) -> Text -> Bool
566 | all = Data.Text.all
567 |
568 |
569 | -- | Converts a `Text` into `Bytes` using UTF-8 encoding.
570 | toBytes :: Text -> Bytes
571 | toBytes text =
572 | text
573 | |> Data.Text.Encoding.encodeUtf8
574 | |> Bytes.INTERNAL_CORE_BYTES_CONSTRUCTOR
575 |
576 |
577 | fromBytes :: Bytes -> Text
578 | fromBytes (Bytes.INTERNAL_CORE_BYTES_CONSTRUCTOR bytes) =
579 | bytes
580 | |> Data.Text.Encoding.decodeUtf8
581 |
582 |
583 | convert :: (IsString s) => Text -> s
584 | convert text = Data.Text.unpack text |> fromString
585 |
586 |
587 | toPascalCase :: Text -> Text
588 | toPascalCase txt =
589 | Manipulate.toPascal txt
590 |
591 |
592 | toCamelCase :: Text -> Text
593 | toCamelCase txt =
594 | Manipulate.toCamel txt
595 |
596 |
597 | toTitleCase :: Text -> Text
598 | toTitleCase txt =
599 | Manipulate.toTitle txt
600 |
601 |
602 | toSnakeCase :: Text -> Text
603 | toSnakeCase txt =
604 | Manipulate.toSnake txt
605 |
606 |
607 | toKebabCase :: Text -> Text
608 | toKebabCase txt =
609 | Manipulate.toSpinal txt
610 |
--------------------------------------------------------------------------------
/core/core/Tuple.hs:
--------------------------------------------------------------------------------
1 | module Tuple (
2 | -- * Create
3 | pair,
4 |
5 | -- * Access
6 | first,
7 | second,
8 |
9 | -- * Map
10 | mapFirst,
11 | mapSecond,
12 | mapBoth,
13 | ) where
14 |
15 |
16 | -- CREATE
17 |
18 | -- | Create a 2-tuple.
19 | --
20 | -- > -- pair 3 4 == (3, 4)
21 | -- >
22 | -- > zip :: List a -> List b -> List (a, b)
23 | -- > zip xs ys =
24 | -- > List.map2 Tuple.pair xs ys
25 | pair :: a -> b -> (a, b)
26 | pair a b =
27 | (a, b)
28 |
29 |
30 | -- ACCESS
31 |
32 | -- | Extract the first value from a tuple.
33 | --
34 | -- > first (3, 4) == 3
35 | -- > first ("john", "doe") == "john"
36 | first :: (a, b) -> a
37 | first (x, _) =
38 | x
39 |
40 |
41 | -- | Extract the second value from a tuple.
42 | --
43 | -- > second (3, 4) == 4
44 | -- > second ("john", "doe") == "doe"
45 | second :: (a, b) -> b
46 | second (_, y) =
47 | y
48 |
49 |
50 | -- MAP
51 |
52 | -- | Transform the first value in a tuple.
53 | --
54 | -- > import String
55 | -- >
56 | -- > mapFirst String.reverse ("stressed", 16) == ("desserts", 16)
57 | -- > mapFirst String.length ("stressed", 16) == (8, 16)
58 | mapFirst :: (a -> x) -> (a, b) -> (x, b)
59 | mapFirst func (x, y) =
60 | (func x, y)
61 |
62 |
63 | -- | Transform the second value in a tuple.
64 | --
65 | -- > mapSecond sqrt ("stressed", 16) == ("stressed", 4)
66 | -- > mapSecond negate ("stressed", 16) == ("stressed", -16)
67 | mapSecond :: (b -> y) -> (a, b) -> (a, y)
68 | mapSecond func (x, y) =
69 | (x, func y)
70 |
71 |
72 | -- | Transform both parts of a tuple.
73 | --
74 | -- > import String
75 | -- >
76 | -- > mapBoth String.reverse sqrt ("stressed", 16) == ("desserts", 4)
77 | -- > mapBoth String.length negate ("stressed", 16) == (8, -16)
78 | mapBoth :: (a -> x) -> (b -> y) -> (a, b) -> (x, y)
79 | mapBoth funcA funcB (x, y) =
80 | (funcA x, funcB y)
81 |
--------------------------------------------------------------------------------
/core/core/Unit.hs:
--------------------------------------------------------------------------------
1 | module Unit (
2 | Unit,
3 | unit,
4 | ) where
5 |
6 |
7 | -- | 'Unit' is a type that has a single value.
8 | -- It is used to represent the return type of
9 | -- functions that do not return anything.
10 | -- In other languages it is usually called `void`.
11 | type Unit = ()
12 |
13 |
14 | unit :: Unit
15 | unit = ()
16 |
--------------------------------------------------------------------------------
/core/core/Unknown.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE AllowAmbiguousTypes #-}
2 |
3 | module Unknown (
4 | Unknown,
5 | fromValue,
6 | toValue,
7 | Convertible,
8 | apply,
9 | getTypeName,
10 | getUnknownTypeName,
11 | ) where
12 |
13 | import Appendable ((++))
14 | import Basics
15 | import Data.Dynamic qualified
16 | import Data.Proxy qualified
17 | import Data.Typeable (TypeRep, Typeable, typeRep)
18 | import Maybe (Maybe)
19 | import Maybe qualified
20 | import Text (Text)
21 | import Text qualified
22 | import ToText
23 |
24 |
25 | -- | The 'Unknown' type represents a dynamically typed value.
26 | data Unknown = Unknown
27 | { value :: Data.Dynamic.Dynamic,
28 | typeRepresentation :: TypeRep
29 | }
30 | deriving (Show, Typeable)
31 |
32 |
33 | -- | The 'Convertible' type class represents types that can be converted to 'Unknown'.
34 | type Convertible value = Typeable value
35 |
36 |
37 | -- | Convert a value of any type to 'Unknown'.
38 | fromValue :: (Typeable value) => value -> Unknown
39 | fromValue value = do
40 | let dynamicValue = Data.Dynamic.toDyn value
41 | Unknown dynamicValue (Data.Dynamic.dynTypeRep dynamicValue)
42 |
43 |
44 | -- | Convert an 'Unknown' value back to its original type, if possible.
45 | toValue :: (Typeable value) => Unknown -> Maybe value
46 | toValue (Unknown dynamic _) =
47 | Data.Dynamic.fromDynamic dynamic
48 |
49 |
50 | -- | Applies a function that is inside the 'Unknown' value.
51 | apply :: Unknown -> Unknown -> Maybe Unknown
52 | apply (Unknown f _) (Unknown x _) =
53 | Data.Dynamic.dynApply f x
54 | |> Maybe.map \result -> Unknown result (Data.Dynamic.dynTypeRep result)
55 |
56 |
57 | -- | Returns the name of the type
58 | getTypeName ::
59 | forall a.
60 | (Typeable a) =>
61 | Text
62 | getTypeName =
63 | typeRep (Data.Proxy.Proxy @a) |> toPrettyText
64 |
65 |
66 | -- | Gets the name of the type of an unknown value
67 | getUnknownTypeName :: Unknown -> Text
68 | getUnknownTypeName (Unknown _ typeRepresentation) =
69 | toPrettyText typeRepresentation
70 |
71 |
72 | instance (Convertible a, Convertible b) => Show (a -> b) where
73 | show _ = do
74 | let t = "(" ++ Unknown.getTypeName @a ++ " -> " ++ Unknown.getTypeName @b ++ ")"
75 | Text.toLinkedList t
76 |
--------------------------------------------------------------------------------
/core/core/Uuid.hs:
--------------------------------------------------------------------------------
1 | module Uuid (
2 | Uuid,
3 | generate,
4 | ) where
5 |
6 | import Basics
7 | import Data.UUID qualified as UUID
8 | import Data.UUID.V4 qualified as V4
9 | import Task (Task)
10 | import Task qualified
11 | import ToText (Show)
12 |
13 |
14 | newtype Uuid = Uuid (UUID.UUID)
15 | deriving (Eq, Ord, Show)
16 |
17 |
18 | generate :: Task _ Uuid
19 | generate = do
20 | uuid <- Task.fromIO V4.nextRandom
21 | Task.yield (Uuid uuid)
22 |
--------------------------------------------------------------------------------
/core/core/Var.hs:
--------------------------------------------------------------------------------
1 | module Var (Var, new, get, set) where
2 |
3 | import Basics
4 | import Data.IORef qualified as GHC
5 | import IO (IO)
6 |
7 |
8 | newtype Var value = Var (GHC.IORef value)
9 |
10 |
11 | new :: value -> IO (Var value)
12 | new value = do
13 | ref <- GHC.newIORef value
14 | pure (Var ref)
15 |
16 |
17 | get :: Var value -> IO value
18 | get (Var ref) = GHC.readIORef ref
19 |
20 |
21 | set :: value -> Var value -> IO ()
22 | set value (Var ref) = GHC.writeIORef ref value
23 |
--------------------------------------------------------------------------------
/core/core/Version.hs:
--------------------------------------------------------------------------------
1 | {-# OPTIONS_GHC -Wno-orphans #-}
2 |
3 | module Version (
4 | Version,
5 | version,
6 | parse,
7 | toText,
8 | ) where
9 |
10 | import Array qualified
11 | import Basics
12 | import Char (Char)
13 | import Control.Monad.Fail qualified as GHC
14 | import Data.List qualified as GHC
15 | import Data.Version (Version (..))
16 | import Data.Version qualified
17 | import GHC.Real qualified as GHC
18 | import Language.Haskell.TH qualified as TH
19 | import Language.Haskell.TH.Quote (QuasiQuoter (..))
20 | import Language.Haskell.TH.Quote qualified as Quote
21 | import Language.Haskell.TH.Syntax qualified as TH
22 | import LinkedList (LinkedList)
23 | import Mappable qualified
24 | import Maybe (Maybe (..))
25 | import Text (Text)
26 | import Text qualified
27 | import Text.ParserCombinators.ReadP qualified as ReadP
28 | import ToText qualified
29 |
30 |
31 | instance TH.Lift Version where
32 | lift (Version branch tags) =
33 | TH.appE
34 | (TH.conE 'Version)
35 | ( branch
36 | |> Mappable.map (GHC.fromIntegral .> TH.integerL)
37 | |> Mappable.map TH.litE
38 | |> TH.listE
39 | )
40 | `TH.appE` ( tags
41 | |> Mappable.map TH.stringL
42 | |> Mappable.map TH.litE
43 | |> TH.listE
44 | )
45 | liftTyped (Version branch tags) =
46 | TH.unsafeCodeCoerce (TH.lift (Version branch tags))
47 |
48 |
49 | -- | Smart text constructor for versions.
50 | --
51 | -- Example:
52 | --
53 | -- @
54 | -- let ver = [version|0.0.0-alpha|]
55 | -- @
56 | --
57 | -- This will create a version with branch [0, 0, 0] and tags ["alpha"].
58 | version :: Quote.QuasiQuoter
59 | version =
60 | QuasiQuoter
61 | { quoteExp = parseVersionExp,
62 | quotePat = \_ -> GHC.fail "Versions can only be used as expressions",
63 | quoteType = \_ -> GHC.fail "Versions can only be used as expressions",
64 | quoteDec = \_ -> GHC.fail "Versions can only be used as expressions"
65 | }
66 |
67 |
68 | -- | Parse a version from a Text.
69 | --
70 | -- Example:
71 | --
72 | -- > Version.parse "0.0.0-alpha"
73 | -- Just (Version [0, 0, 0] ["alpha"])
74 | parse :: Text -> Maybe Version
75 | parse input =
76 | case ReadP.readP_to_S Data.Version.parseVersion (Text.toLinkedList input) of
77 | [] -> Nothing
78 | parses ->
79 | case GHC.find (\(_, remaining) -> remaining == "") parses of
80 | Just (v, "") -> Just v
81 | _ -> Nothing
82 |
83 |
84 | parseVersionExp :: LinkedList Char -> TH.Q TH.Exp
85 | parseVersionExp input =
86 | case ReadP.readP_to_S Data.Version.parseVersion input of
87 | [] -> GHC.fail "Invalid version format: no valid parse"
88 | parses ->
89 | case GHC.find (\(_, remaining) -> remaining == "") parses of
90 | Just (v, "") -> [|v|]
91 | _ -> GHC.fail "Invalid version format, must be in the form A.B.C-XYZ"
92 |
93 |
94 | toText :: Version -> Text
95 | toText (Version branch tags) = do
96 | let textTags =
97 | Array.fromLinkedList tags
98 | |> Array.map Text.fromLinkedList
99 | |> Text.joinWith "-"
100 | let textBranch =
101 | Array.fromLinkedList branch
102 | |> Array.map ToText.toText
103 | |> Text.joinWith "."
104 | Array.fromLinkedList [textBranch, textTags]
105 | |> Array.dropIf Text.isEmpty
106 | |> Text.joinWith "-"
107 |
--------------------------------------------------------------------------------
/core/default.nix:
--------------------------------------------------------------------------------
1 | { pkgs ? import ../nix/nixpkgs.nix { } }:
2 | pkgs.haskellPackages.developPackage {
3 | root = ./.;
4 | source-overrides = { };
5 | modifier = drv: pkgs.haskell.lib.dontHaddock drv;
6 | }
7 |
--------------------------------------------------------------------------------
/core/http/Http.hs:
--------------------------------------------------------------------------------
1 | module Http (
2 | module Reexported,
3 | ) where
4 |
5 | import Http.Client as Reexported
6 |
7 |
--------------------------------------------------------------------------------
/core/http/Http/Client.hs:
--------------------------------------------------------------------------------
1 | module Http.Client (
2 | Request (..),
3 | Error (..),
4 | get,
5 | post,
6 | request,
7 | withUrl,
8 | addHeader,
9 | ) where
10 |
11 | import Core
12 | import Json qualified
13 | import Map qualified
14 | import Maybe qualified
15 | import Network.HTTP.Simple qualified as Http
16 | import Network.HTTP.Simple qualified as HttpSimple
17 | import Task qualified
18 | import Text qualified
19 |
20 |
21 | data Request = Request
22 | { url :: Maybe Text,
23 | headers :: Map Text Text
24 | }
25 | deriving (Show)
26 |
27 |
28 | instance Default Request where
29 | def =
30 | Request
31 | { url = Nothing,
32 | headers = Map.empty
33 | }
34 |
35 |
36 | request :: Request
37 | request = def
38 |
39 |
40 | withUrl :: Text -> Request -> Request
41 | withUrl url options =
42 | options
43 | { url = Just url
44 | }
45 |
46 |
47 | addHeader :: Text -> Text -> Request -> Request
48 | addHeader key value options =
49 | options
50 | { headers = options.headers |> Map.set key value
51 | }
52 |
53 |
54 | data Error = Error Text
55 | deriving (Show)
56 |
57 |
58 | get ::
59 | (Json.FromJSON response) =>
60 | Request ->
61 | Task Error response
62 | get options = Task.fromIO do
63 | let url = options.url |> Maybe.withDefault (panic "url is required")
64 |
65 | log "Parsing request"
66 | r <- Text.toLinkedList url |> HttpSimple.parseRequest
67 |
68 | log "Setting headers"
69 | let req =
70 | options.headers
71 | |> Map.reduce r \key value acc ->
72 | HttpSimple.addRequestHeader (Text.convert key) (Text.convert value) acc
73 |
74 | log "Performing request"
75 | response <- HttpSimple.httpJSON req
76 |
77 | log "Returning"
78 | Http.getResponseBody response
79 | |> pure
80 |
81 |
82 | -- | Performs a POST request
83 | post ::
84 | (Json.FromJSON response, Json.ToJSON requestBody) =>
85 | Request ->
86 | requestBody ->
87 | Task Error response
88 | post options body = Task.fromIO do
89 | let url = options.url |> Maybe.withDefault (panic "url is required")
90 |
91 | log "Parsing request"
92 | r <- Text.toLinkedList url |> HttpSimple.parseRequest
93 |
94 | log "Setting headers"
95 | let req =
96 | options.headers
97 | |> Map.reduce r \key value acc ->
98 | HttpSimple.addRequestHeader (Text.convert key) (Text.convert value) acc
99 | |> HttpSimple.setRequestMethod "POST"
100 | |> HttpSimple.setRequestBodyJSON body
101 |
102 | log "Performing request"
103 | response <- HttpSimple.httpJSON req
104 |
105 | log "Returning"
106 | Http.getResponseBody response
107 | |> pure
108 |
--------------------------------------------------------------------------------
/core/json/Json.hs:
--------------------------------------------------------------------------------
1 | module Json (
2 | Decodable,
3 | Encodable,
4 | Aeson.Value,
5 | Aeson.FromJSON,
6 | Aeson.FromJSONKey,
7 | Aeson.ToJSON,
8 | decodeText,
9 | encode,
10 | decode,
11 | encodeText,
12 | ) where
13 |
14 | import Array (Array)
15 | import Basics
16 | import Data.Aeson qualified as Aeson
17 | import Data.Aeson.Text qualified as AesonText
18 | import Data.Either qualified as Either
19 | import Data.Text.Lazy qualified as Data.Text
20 | import Result (Result)
21 | import Result qualified
22 | import Text (Text)
23 | import Text qualified
24 |
25 |
26 | type Decodable value = Aeson.FromJSON value
27 |
28 |
29 | type Encodable value = Aeson.ToJSON value
30 |
31 |
32 | decodeText :: (Decodable value) => Text -> Result Text value
33 | decodeText text = do
34 | let bs = Text.convert text
35 | case Aeson.eitherDecodeStrict bs of
36 | Either.Left error -> Result.Err (Text.fromLinkedList error)
37 | Either.Right value -> Result.Ok value
38 |
39 |
40 | encodeText :: (Encodable value) => value -> Text
41 | encodeText value =
42 | AesonText.encodeToLazyText value
43 | |> Data.Text.toStrict
44 |
45 |
46 | encode :: (Encodable value) => value -> Aeson.Value
47 | encode = Aeson.toJSON
48 |
49 |
50 | decode :: (Decodable value) => Aeson.Value -> Result Text value
51 | decode value = case Aeson.fromJSON value of
52 | Aeson.Error error -> Result.Err (Text.fromLinkedList error)
53 | Aeson.Success val -> Result.Ok val
54 |
55 |
56 | instance (Aeson.FromJSON a) => Aeson.FromJSON (Array a)
57 |
58 |
59 | instance (Aeson.ToJSON a) => Aeson.ToJSON (Array a)
60 |
--------------------------------------------------------------------------------
/core/nhcore.cabal:
--------------------------------------------------------------------------------
1 | cabal-version: 3.4
2 | name: nhcore
3 | version: 0.6.0
4 | synopsis: Core Library for NeoHaskell
5 | description: NeoHaskell is a dialect of Haskell that is focused on newcomer-friendliness and productivity.
6 | homepage: https://neohaskell.org
7 | license: Apache-2.0
8 | author: Nikita Tchayka
9 | maintainer: nhlib@nickseagull.dev
10 | copyright:
11 | category: Development
12 | build-type: Simple
13 |
14 | common common_cfg
15 | ghc-options: -Wall
16 | -Wno-orphans
17 | -fno-warn-partial-type-signatures
18 | -fno-warn-name-shadowing
19 | -Werror
20 | build-depends:
21 | base ^>=4.18,
22 | ghc-prim ,
23 | async ,
24 | aeson ,
25 | brick ,
26 | vty ,
27 | bytestring ,
28 | data-default ,
29 | text ,
30 | vector ,
31 | directory ,
32 | text-manipulate ,
33 | transformers ,
34 | http-conduit ,
35 | containers ,
36 | optparse-applicative ,
37 | path ,
38 | nanotime ,
39 | process ,
40 | pretty-simple ,
41 | unagi-chan ,
42 | QuickCheck ,
43 | quickcheck-instances ,
44 | PyF ,
45 | with-utf8 ,
46 | dir-traverse ,
47 | Cabal ,
48 | uuid ,
49 | template-haskell
50 |
51 |
52 | default-extensions:
53 | ApplicativeDo
54 | BlockArguments
55 | DataKinds
56 | NoImplicitPrelude
57 | TemplateHaskell
58 | DeriveDataTypeable
59 | QuasiQuotes
60 | QualifiedDo
61 | ImpredicativeTypes
62 | ImportQualifiedPost
63 | OverloadedStrings
64 | OverloadedLabels
65 | OverloadedRecordDot
66 | DuplicateRecordFields
67 | PackageImports
68 | NamedFieldPuns
69 | Strict
70 | TypeFamilies
71 | PartialTypeSignatures
72 |
73 | library
74 | import: common_cfg
75 | exposed-modules:
76 | -- Core
77 | Core,
78 | Char,
79 | Accumulator,
80 | Basics,
81 | Bytes,
82 | Text,
83 | Function,
84 | Map,
85 | LinkedList,
86 | Result,
87 | Unit,
88 | Uuid,
89 | Tuple,
90 | Console,
91 | Int,
92 | IO,
93 | Maybe,
94 | Array,
95 | Version,
96 | Record,
97 | Unknown,
98 | Var,
99 | Task,
100 | Toml,
101 |
102 | -- System
103 | File,
104 | Directory,
105 | Subprocess,
106 | Path,
107 | Time,
108 |
109 | -- OptionsParser
110 | Command,
111 |
112 | -- JSON
113 | Json,
114 |
115 | -- Traits
116 | Applicable,
117 | Appendable,
118 | Combinable,
119 | Default,
120 | Mappable,
121 | Thenable,
122 | ToText,
123 |
124 | -- Service
125 | Action,
126 | Trigger,
127 | Service.Event,
128 | Service.EventStore,
129 |
130 | -- Concurrency
131 | AsyncIO,
132 | Channel,
133 | ConcurrentVar,
134 |
135 | -- Http
136 | Http,
137 | Http.Client
138 |
139 |
140 | -- other-modules:
141 | -- other-extensions:
142 | hs-source-dirs:
143 | core,
144 | concurrency,
145 | service,
146 | json,
147 | toml,
148 | traits,
149 | system,
150 | http,
151 | options-parser
152 | default-language: GHC2021
153 |
154 | test-suite nhcore-test
155 | import: common_cfg
156 | default-language: GHC2021
157 | -- other-modules:
158 | -- other-extensions:
159 | type: exitcode-stdio-1.0
160 | hs-source-dirs: test
161 | main-is: Main.hs
162 | build-depends:
163 | base ^>=4.18,
164 | nhcore
165 |
--------------------------------------------------------------------------------
/core/options-parser/Command.hs:
--------------------------------------------------------------------------------
1 | module Command (
2 | OptionsParser,
3 | CommandOptions (..),
4 | PathConfig (..),
5 | TextConfig (..),
6 | text,
7 | path,
8 | parseWith,
9 | json,
10 | flag,
11 | parse,
12 | parseHandler,
13 | commands,
14 | map,
15 | ) where
16 |
17 | import Action (Action)
18 | import Action qualified
19 | import Appendable ((++))
20 | import Array (Array)
21 | import Array qualified
22 | import Basics
23 | import Char (Char)
24 | import Combinable (Combinable)
25 | import Combinable qualified
26 | import Control.Applicative qualified as Applicative
27 | import Data.Aeson qualified as Json
28 | import Data.Either qualified as GHC
29 | import Data.Functor qualified as Functor
30 | import Data.Version (Version)
31 | import Default (Default (..), defaultValue)
32 | import LinkedList (LinkedList)
33 | import Mappable qualified
34 | import Maybe (Maybe (..))
35 | import Maybe qualified
36 | import Options.Applicative qualified as OptParse
37 | import Path (Path)
38 | import Result (Result (..))
39 | import Task (Task)
40 | import Task qualified
41 | import Text (Text, fromLinkedList, toLinkedList)
42 | import Text qualified
43 | import ToText (Show (..), ToText)
44 | import Unknown qualified
45 | import Version qualified
46 |
47 |
48 | newtype OptionsParser value = OptionsParser (OptParse.Parser value)
49 | deriving (Functor.Functor, Applicative.Applicative)
50 |
51 |
52 | map :: (a -> b) -> OptionsParser a -> OptionsParser b
53 | map f (OptionsParser parser) = OptionsParser (Mappable.fmap f parser)
54 |
55 |
56 | instance (Unknown.Convertible value) => Show (OptionsParser value) where
57 | show _ = do
58 | let typeName = Unknown.getTypeName @value
59 | "[OptionsParser " ++ Text.toLinkedList typeName ++ "]"
60 |
61 |
62 | data CommandOptions value = CommandOptions
63 | { name :: Text,
64 | description :: Text,
65 | version :: Maybe Version,
66 | decoder :: OptionsParser value
67 | }
68 | deriving (Show)
69 |
70 |
71 | newtype Error = Error Text
72 | deriving (Show)
73 |
74 |
75 | parse ::
76 | (Unknown.Convertible event) =>
77 | CommandOptions event ->
78 | Action event
79 | parse options = Action.named "Command.parse" options
80 |
81 |
82 | parseHandler :: CommandOptions event -> Task _ event
83 | parseHandler options = do
84 | let (OptionsParser parser) = options.decoder
85 | let ver =
86 | options.version
87 | |> Maybe.withDefault [Version.version|0.0.0|]
88 | |> Version.toText
89 | let programDescription =
90 | [fmt|{options.description} - Version {ver}|]
91 | |> Text.toLinkedList
92 | let foo = OptParse.info (parser OptParse.<**> OptParse.helper) (OptParse.progDesc programDescription)
93 | OptParse.execParser foo
94 | |> Task.fromIO
95 |
96 |
97 | data TextConfig = TextConfig
98 | { help :: Text,
99 | long :: Text,
100 | short :: Char,
101 | metavar :: Text,
102 | value :: Maybe Text
103 | }
104 |
105 |
106 | defaultTextConfig :: TextConfig
107 | defaultTextConfig =
108 | TextConfig
109 | { help = defaultValue,
110 | long = defaultValue,
111 | short = defaultValue,
112 | metavar = defaultValue,
113 | value = Nothing
114 | }
115 |
116 |
117 | instance Default TextConfig where
118 | def = defaultTextConfig
119 |
120 |
121 | text :: TextConfig -> OptionsParser Text
122 | text config =
123 | do
124 | let textValue = case config.value of
125 | Just val -> [OptParse.value val]
126 | Nothing -> []
127 | let options =
128 | [ OptParse.help (config.help |> Text.toLinkedList),
129 | OptParse.long (config.long |> Text.toLinkedList),
130 | OptParse.short config.short,
131 | OptParse.metavar (config.metavar |> Text.toLinkedList)
132 | ]
133 | ++ textValue
134 | |> setting
135 |
136 | OptParse.option OptParse.str options
137 | |> OptionsParser
138 |
139 |
140 | data PathConfig = PathConfig
141 | { help :: Text,
142 | long :: Text,
143 | short :: Char,
144 | metavar :: Text,
145 | value :: Maybe Path
146 | }
147 |
148 |
149 | defaultPathConfig :: PathConfig
150 | defaultPathConfig =
151 | PathConfig
152 | { help = defaultValue,
153 | long = defaultValue,
154 | short = defaultValue,
155 | metavar = defaultValue,
156 | value = Nothing
157 | }
158 |
159 |
160 | instance Default PathConfig where
161 | def = defaultPathConfig
162 |
163 |
164 | path :: PathConfig -> OptionsParser Path
165 | path config = do
166 | let pathValue = case config.value of
167 | Just val -> [OptParse.value val]
168 | Nothing -> []
169 | let options =
170 | [ OptParse.help (config.help |> Text.toLinkedList),
171 | OptParse.long (config.long |> Text.toLinkedList),
172 | OptParse.short config.short,
173 | OptParse.metavar (config.metavar |> Text.toLinkedList)
174 | ]
175 | setting (pathValue ++ options)
176 | |> OptParse.option OptParse.str
177 | |> OptionsParser
178 |
179 |
180 | data JsonConfig value = JsonConfig
181 | { help :: Text,
182 | long :: Text,
183 | short :: Char,
184 | metavar :: Text,
185 | value :: Maybe value
186 | }
187 |
188 |
189 | defaultJsonConfig :: (Default value) => (JsonConfig value)
190 | defaultJsonConfig =
191 | JsonConfig
192 | { help = defaultValue,
193 | long = defaultValue,
194 | short = defaultValue,
195 | metavar = defaultValue,
196 | value = Nothing
197 | }
198 |
199 |
200 | instance (Default value) => Default (JsonConfig value) where
201 | def = defaultJsonConfig
202 |
203 |
204 | json ::
205 | ( Default value,
206 | ToText value,
207 | Eq value,
208 | Json.FromJSON value
209 | ) =>
210 | JsonConfig value ->
211 | OptionsParser value
212 | json config = do
213 | let parseFunction textToParse = do
214 | let either = Json.eitherDecodeStrict (Text.convert textToParse)
215 | case either of
216 | GHC.Left err -> Err (Text.fromLinkedList err)
217 | GHC.Right val -> Ok val
218 | parseWith (parseFunction) config
219 |
220 |
221 | data FlagConfig = FlagConfig
222 | { help :: Text,
223 | long :: Text,
224 | short :: Char,
225 | value :: Maybe Bool
226 | }
227 |
228 |
229 | defaultFlagConfig :: FlagConfig
230 | defaultFlagConfig =
231 | FlagConfig
232 | { help = defaultValue,
233 | long = defaultValue,
234 | short = defaultValue,
235 | value = defaultValue
236 | }
237 |
238 |
239 | instance Default FlagConfig where
240 | def = defaultFlagConfig
241 |
242 |
243 | flag :: FlagConfig -> OptionsParser Bool
244 | flag config = do
245 | setting
246 | [ OptParse.help (config.help |> Text.toLinkedList),
247 | OptParse.long (config.long |> Text.toLinkedList),
248 | OptParse.short config.short
249 | ]
250 | |> OptParse.switch
251 | |> OptionsParser
252 |
253 |
254 | parseWith ::
255 | (ToText value) =>
256 | (Text -> Result Text value) ->
257 | JsonConfig value ->
258 | OptionsParser value
259 | parseWith parseFunc config = do
260 | let wrappedParseFunction charList = do
261 | let textToParse = Text.fromLinkedList charList
262 | let result = parseFunc textToParse
263 | resultToEither result
264 |
265 | let reader = OptParse.eitherReader wrappedParseFunction
266 |
267 | let defaultValueConfig = case config.value of
268 | Just val -> [OptParse.value val]
269 | Nothing -> []
270 |
271 | let options =
272 | [ OptParse.help (config.help |> Text.toLinkedList),
273 | OptParse.long (config.long |> Text.toLinkedList),
274 | OptParse.short config.short,
275 | OptParse.metavar (config.metavar |> Text.toLinkedList)
276 | ]
277 |
278 | setting (defaultValueConfig ++ options)
279 | |> OptParse.option reader
280 | |> OptionsParser
281 |
282 |
283 | resultToEither :: Result Text value -> GHC.Either (LinkedList Char) value
284 | resultToEither (Ok val) = GHC.Right val
285 | resultToEither (Err err) = GHC.Left (Text.toLinkedList err)
286 |
287 |
288 | commands :: Array (CommandOptions value) -> OptionsParser value
289 | commands commandConfigs = do
290 | let cmds =
291 | commandConfigs
292 | |> Array.map
293 | ( \config -> do
294 | let (OptionsParser handler) = config.decoder
295 | let name = (config.name |> Text.toLinkedList)
296 | let description = OptParse.info handler (config.description |> Text.toLinkedList |> OptParse.progDesc)
297 | OptParse.command name description
298 | )
299 | |> Array.toLinkedList
300 | |> Combinable.mconcat
301 |
302 | OptionsParser (OptParse.subparser cmds)
303 |
304 |
305 | -- Private
306 |
307 | -- Helper function to make porting to opt-env-conf in the future easily
308 | setting :: (Combinable m) => [m] -> m
309 | setting = Combinable.mconcat
310 |
--------------------------------------------------------------------------------
/core/service/Action.hs:
--------------------------------------------------------------------------------
1 | module Action (
2 | Action (..),
3 | Handler,
4 | HandlerRegistry,
5 | emptyRegistry,
6 | none,
7 | batch,
8 | map,
9 | named,
10 | processBatch,
11 | continueWith,
12 | continueWithHandler,
13 | ActionResult (..),
14 | ) where
15 |
16 | import Array (Array)
17 | import Array qualified
18 | import Basics
19 | import Console (log)
20 | import IO (IO)
21 | import IO qualified
22 | import Map (Map)
23 | import Map qualified
24 | import Maybe (Maybe (..))
25 | import Text (Text)
26 | import ToText (Show (..), ToText, toPrettyText)
27 | import Unknown (Unknown)
28 | import Unknown qualified
29 | import Var (Var)
30 | import Var qualified
31 |
32 |
33 | {-
34 | Actions (Action) are essentially a callback that is called after some side effect
35 | happens. First, the platform, or the user, will register action handlers that will
36 | act as effectful functions that will be triggered when the action is submitted
37 | into the Action queue (this is hidden from the user, and handled by the platform),
38 | then, when the user returns a action of this kind either in the init function or
39 | the update function, the platform will submit the action to the queue, and the
40 | handler will be called with the action as an argument. Usually the action constructors
41 | are basically functions that configure everything to be ready to be executed.
42 |
43 | For example, one might want to read a file right when the app starts, so one would do
44 | this in the init function by using a hypothetical FileSystem.readFile action, configured
45 | with the path to the file. If the user desires to handle the file contents in a certain
46 | event, they would have to use Action.map to transform the file contents into a different
47 | action that will be called when the event happens.
48 |
49 | Example:
50 |
51 | type Model = [
52 | "someText" := Text
53 | ]
54 |
55 | data Event
56 | = AppStarted String
57 |
58 | init :: (Model, Action Event)
59 | init = (
60 | emptyModel,
61 | FileSystem.readFile "someFile.txt"
62 | |> Action.map AppStarted
63 | )
64 |
65 | The advantage of defining side effects through the usage of actions is that it
66 | allows the side effects to be tracked in a queue, which can be inspected in many different
67 | ways, and also test them in a controlled environment.
68 |
69 | This allows for even more interesting features, like allowing to replay the actions without
70 | actually executing them, or even to serialize them and send them to another platform to be
71 | executed there. This is the basis for the time-travel debugging feature that Elm has, and
72 | a great inspiration for the NeoHaskell platform.
73 | -}
74 | newtype Action event = Action (Array ActionRecord)
75 | deriving (Show)
76 |
77 |
78 | data ActionRecord = ActionRecord
79 | { name :: ActionName,
80 | payload :: Unknown
81 | }
82 | deriving (Show)
83 |
84 |
85 | data ActionName
86 | = Custom Text
87 | | MapAction
88 | deriving (Show)
89 |
90 |
91 | type Handler = Unknown -> IO (Maybe Unknown)
92 |
93 |
94 | type HandlerRegistry = Map Text Handler
95 |
96 |
97 | emptyRegistry :: Action.HandlerRegistry
98 | emptyRegistry = Map.empty
99 |
100 |
101 | none :: Action event
102 | none = Action (Array.empty)
103 |
104 |
105 | batch :: Array (Action event) -> Action event
106 | batch actions =
107 | actions
108 | |> Array.flatMap (\(Action action) -> action)
109 | |> Action
110 |
111 |
112 | map :: (Unknown.Convertible a, Unknown.Convertible b) => (a -> b) -> Action a -> Action b
113 | map f (Action actions) =
114 | actions
115 | |> Array.push (ActionRecord {name = MapAction, payload = Unknown.fromValue f})
116 | |> Action
117 |
118 |
119 | data ActionResult
120 | = Continue (Maybe Unknown)
121 | | Error Text
122 |
123 |
124 | processBatch ::
125 | HandlerRegistry ->
126 | Action anyEvent ->
127 | IO (ActionResult)
128 | processBatch registry (Action actionBatch) = do
129 | log "[processBatch] Processing batch"
130 | currentOutput <- Var.new Nothing
131 |
132 | log [fmt|[processBatch] Starting action loop with {Array.length actionBatch} actions|]
133 | result <- actionBatch |> Array.foldM (processAction currentOutput) (Continue Nothing)
134 |
135 | case result of
136 | Continue _ -> do
137 | log "[processBatch] Getting final output"
138 | out <- Var.get currentOutput
139 | pure (Continue out) -- Output is still Unknown
140 | Error msg -> pure (Error msg)
141 | where
142 | processAction :: Var (Maybe Unknown) -> ActionResult -> ActionRecord -> IO (ActionResult)
143 | processAction _ (Error msg) _ = pure (Error msg)
144 | processAction currentOutput (Continue _) action = do
145 | if shouldExit action.name
146 | then IO.exitSuccess
147 | else case action.name of
148 | Custom name' -> handleCustomAction name' action.payload currentOutput
149 | MapAction -> handleMapAction action.payload currentOutput
150 |
151 | handleCustomAction :: Text -> Unknown -> Var (Maybe Unknown) -> IO (ActionResult)
152 | handleCustomAction name' payload currentOutput = do
153 | case Map.get name' registry of
154 | Just handler -> do
155 | log [fmt|Found handler for {name'}, calling with {toPrettyText payload}|]
156 | result <- handler payload
157 | log [fmt|Handler {name'} returned: {toPrettyText result}|]
158 | case result of
159 | Nothing -> do
160 | log [fmt|Handler {name'} returned nothing|]
161 | pure (Continue Nothing)
162 | Just result' -> do
163 | log [fmt|Setting output to {toPrettyText result'}|]
164 | currentOutput |> Var.set (Just result')
165 | pure (Continue (Just result'))
166 | Nothing -> pure (Error [fmt|Action handler not found for: {name'}|])
167 |
168 | handleMapAction :: Unknown -> Var (Maybe Unknown) -> IO (ActionResult)
169 | handleMapAction payload currentOutput = do
170 | log "[processBatch] Map action"
171 | maybeOut <- Var.get currentOutput
172 | case maybeOut of
173 | Nothing -> pure (Error "[processBatch] No output to map")
174 | Just out -> do
175 | log "[processBatch] Applying mapping function"
176 | -- Apply the mapping function directly to the Unknown values
177 | case Unknown.apply payload out of
178 | Nothing -> do
179 | log "[processBatch] Couldn't apply mapping function"
180 | pure (Error "Couldn't apply mapping function")
181 | Just output -> do
182 | log "[processBatch] Setting output"
183 | currentOutput |> Var.set (Just output)
184 | pure (Continue (Just output))
185 |
186 | shouldExit :: ActionName -> Bool
187 | shouldExit actionName =
188 | case actionName of
189 | Custom "exit" -> True
190 | _ -> False
191 |
192 |
193 | named ::
194 | (Unknown.Convertible value) =>
195 | Text ->
196 | value ->
197 | Action result
198 | named name value =
199 | Array.fromLinkedList [(ActionRecord {name = Custom name, payload = Unknown.fromValue value})]
200 | |> Action
201 |
202 |
203 | continueWith ::
204 | (Unknown.Convertible event) =>
205 | event ->
206 | Action event
207 | continueWith event =
208 | named "continueWith" event
209 |
210 |
211 | continueWithHandler ::
212 | forall event.
213 | (Unknown.Convertible event, ToText event) =>
214 | event ->
215 | IO event
216 | continueWithHandler event = do
217 | log [fmt|Continuing with {toPrettyText event}|]
218 | pure event
219 |
--------------------------------------------------------------------------------
/core/service/Service/Event.hs:
--------------------------------------------------------------------------------
1 | module Service.Event (
2 | Event,
3 | StreamId (..),
4 | StreamPosition (..),
5 | ) where
6 |
7 | import Core
8 |
9 |
10 | data Event = Event
11 | { id :: Uuid,
12 | streamId :: StreamId,
13 | position :: StreamPosition
14 | }
15 | deriving (Eq, Show, Ord, Generic)
16 |
17 |
18 | newtype StreamId = StreamId Text
19 | deriving (Eq, Show, Ord, Generic)
20 |
21 |
22 | newtype StreamPosition = StreamPosition (Positive Int)
23 | deriving (Eq, Show, Ord, Generic)
24 |
--------------------------------------------------------------------------------
/core/service/Service/EventStore.hs:
--------------------------------------------------------------------------------
1 | module Service.EventStore (
2 | EventStore (..),
3 | Limit (..),
4 | Error (..),
5 | ) where
6 |
7 | import Core
8 | import Service.Event (Event, StreamId, StreamPosition)
9 |
10 |
11 | newtype Limit = Limit (Positive Int)
12 | deriving (Eq, Show, Ord, Generic)
13 |
14 |
15 | data Error
16 | = StreamNotFound StreamId
17 | | EventNotFound StreamId StreamPosition
18 | | ConcurrencyConflict StreamId StreamPosition StreamPosition -- expected vs actual
19 | | StorageFailure Text -- Generic storage errors with message
20 | deriving (Eq, Show)
21 |
22 |
23 | -- | An interface for the operations of an event store
24 | data EventStore = EventStore
25 | { -- | Append an event to a stream at a given expected revision.
26 | -- Returns the next stream revision if successful, or an Error on conflict or failure.
27 | appendToStream :: StreamId -> StreamPosition -> Event -> Task Error StreamPosition,
28 | -- | Read events from a stream in forward direction starting from a given revision.
29 | -- Returns an array of events or an Error.
30 | readStreamForwardFrom :: StreamId -> StreamPosition -> Limit -> Task Error (Array Event),
31 | -- | Read events from a stream in backward direction starting from a given revision.
32 | -- Useful for looking at recent events.
33 | readStreamBackwardFrom :: StreamId -> StreamPosition -> Limit -> Task Error (Array Event),
34 | -- | Read all events from a given stream.
35 | -- Equivalent to calling 'readStreamForwardFrom' with revision 0 and a high max count.
36 | readAllStreamEvents :: StreamId -> Task Error (Array Event),
37 | -- | Read a slice of all events across all streams starting from a global position.
38 | -- Useful for projections or audit logs.
39 | readAllEventsForwardFrom :: StreamPosition -> Limit -> Task Error (Array Event)
40 | }
41 |
--------------------------------------------------------------------------------
/core/service/Trigger.hs:
--------------------------------------------------------------------------------
1 | module Trigger (
2 | Trigger (..),
3 | new,
4 | ) where
5 |
6 | import Basics
7 | import IO (IO)
8 |
9 |
10 | -- |
11 | -- A trigger is a way to listen to a process that's running in the background and
12 | -- generates events. When you tell NeoHaskell to subscribe to a process, NeoHaskell will
13 | -- pass a callback function that posts an event to the event queue whenever the process
14 | -- generates an event. The callback function is called a `dispatch` function.
15 | --
16 | -- An example Trigger is `Time.every`, which generates an event every specified
17 | -- milliseconds. When you subscribe to `Time.every`, you pass a callback function that
18 | -- posts an event to the event queue whenever the process ticks.
19 | newtype Trigger (event :: Type) = Trigger ((event -> IO ()) -> IO ())
20 |
21 |
22 | new ::
23 | forall (event :: Type).
24 | ((event -> IO ()) -> IO ()) ->
25 | Trigger event
26 | new processConstructor = Trigger processConstructor
27 |
--------------------------------------------------------------------------------
/core/system/Directory.hs:
--------------------------------------------------------------------------------
1 | module Directory (
2 | Error (..),
3 | createAction,
4 | CreateOptions (..),
5 | create,
6 | walk,
7 | copy,
8 | ) where
9 |
10 | import Action (Action)
11 | import Action qualified
12 | import Array (Array)
13 | import Array qualified
14 | import Basics
15 | import Console qualified
16 | import Control.Exception qualified as Exception
17 | import Data.Either qualified as Either
18 | import Distribution.Simple.Utils qualified as Cabal
19 | import Distribution.Verbosity qualified as CabalVerbosity
20 | import GHC.IO.Exception qualified as Exception
21 | import Maybe qualified
22 | import Path (Path)
23 | import Path qualified
24 | import Result (Result (..))
25 | import System.Directory qualified
26 | import System.Directory.Internal.Prelude qualified as Exception
27 | import System.Directory.Recursive qualified as RecursiveDir
28 | import System.IO.Error (alreadyExistsErrorType)
29 | import Task (Task)
30 | import Task qualified
31 | import Text qualified
32 | import ToText (Show (..))
33 | import ToText qualified
34 |
35 |
36 | data Error
37 | = NotFound
38 | | NotWritable
39 | | NotReadable
40 | | WalkError
41 | deriving (Show)
42 |
43 |
44 | data CreateOptions = CreateOptions
45 | { path :: Path
46 | }
47 | deriving (Show)
48 |
49 |
50 | createAction :: CreateOptions -> Action (Result Error Unit)
51 | createAction options =
52 | Action.named "Directory.create" options
53 |
54 |
55 | create :: Path -> Task Error Unit
56 | create dirPath = do
57 | let log m = Console.log m |> Task.fromIO
58 | let p = Path.toText dirPath
59 | log [fmt|[[Directory.create] Attempting to create directory: {p}|]
60 | let createDirAction =
61 | dirPath
62 | |> Path.toLinkedList
63 | |> System.Directory.createDirectory
64 | result <- Exception.try @Exception.IOError createDirAction |> Task.fromIO
65 | case result of
66 | Either.Left err ->
67 | if Exception.ioeGetErrorType err == alreadyExistsErrorType
68 | then do
69 | pure unit
70 | else do
71 | log "[Directory.create] Failed to create directory"
72 | Task.throw NotWritable
73 | Either.Right _ -> do
74 | log [fmt|[[Directory.create] Directory created: {p}|]
75 | Task.yield unit
76 |
77 |
78 | copy :: Path -> Path -> Task Error Unit
79 | copy src dest = do
80 | let log m = Console.log m |> Task.fromIO
81 | let srcP = Path.toText src
82 | let destP = Path.toText dest
83 | log [fmt|[[Directory.copy] Attempting to copy {srcP} to {destP}|]
84 | let copyFileAction =
85 | dest
86 | |> Path.toLinkedList
87 | |> Cabal.copyDirectoryRecursive CabalVerbosity.silent (Path.toLinkedList src)
88 | result <- Exception.try @Exception.IOError copyFileAction |> Task.fromIO
89 | case result of
90 | Either.Left err -> do
91 | log [fmt|[Directory.copy] Failed to copy {srcP} to {destP}: {show err}|]
92 | Task.throw NotWritable
93 | Either.Right _ -> do
94 | log [fmt|[Directory.copy] Copied {srcP} to {destP}|]
95 | Task.yield unit
96 |
97 |
98 | walk :: Path -> Task Error (Array Path)
99 | walk dirPath = do
100 | let log m = Console.log m |> Task.fromIO
101 | let p = Path.toText dirPath
102 | let walkDirAction =
103 | dirPath
104 | |> Path.toLinkedList
105 | |> RecursiveDir.getDirRecursive
106 | result <- Exception.try @Exception.IOError walkDirAction |> Task.fromIO
107 | case result of
108 | Either.Left err -> do
109 | log [fmt|[Directory.walk] Failed to walk directory {p}: {show err}|]
110 | Task.throw WalkError
111 | Either.Right fps -> do
112 | log [fmt|[Directory.walk] Directory {p} walked|]
113 | let paths = Array.fromLinkedList fps
114 | let numPathChars =
115 | if Text.endsWith "/" p
116 | then Text.length p
117 | else Text.length p + 1
118 | let trimInitialSrc pathElement =
119 | Text.fromLinkedList pathElement
120 | |> Text.dropLeft numPathChars
121 | |> Path.fromText
122 | |> Maybe.getOrDie
123 | log [fmt|[Directory.walk] Paths: {ToText.toText paths}|]
124 | let trimmedPaths = paths |> Array.map trimInitialSrc
125 | log [fmt|[Directory.walk] Trimmed paths: {ToText.toText trimmedPaths}|]
126 | Task.yield trimmedPaths
127 |
--------------------------------------------------------------------------------
/core/system/File.hs:
--------------------------------------------------------------------------------
1 | module File (
2 | Error (..),
3 | readText,
4 | writeText,
5 | ) where
6 |
7 | import Basics
8 | import Data.Text.IO qualified as TIO
9 | import GHC.IO.Exception qualified as Exception
10 | import Path (Path)
11 | import Path qualified
12 | import Task (Task)
13 | import Task qualified
14 | import Text (Text)
15 | import ToText (Show (..))
16 |
17 |
18 | data Error
19 | = NotFound
20 | | NotWritable
21 | | NotReadable
22 | deriving (Show)
23 |
24 |
25 | readText :: Path -> Task Error Text
26 | readText filepath =
27 | filepath
28 | |> Path.toLinkedList
29 | |> TIO.readFile
30 | |> Task.fromFailableIO @Exception.IOError
31 | |> Task.mapError (\_ -> NotReadable)
32 |
33 |
34 | writeText :: Path -> Text -> Task Error ()
35 | writeText path textToWrite =
36 | TIO.writeFile (Path.toLinkedList path) textToWrite
37 | |> Task.fromFailableIO @Exception.IOError
38 | |> Task.mapError (\_ -> NotWritable)
39 |
--------------------------------------------------------------------------------
/core/system/Path.hs:
--------------------------------------------------------------------------------
1 | module Path (
2 | Path,
3 | fromText,
4 | fromLinkedList,
5 | toLinkedList,
6 | toText,
7 | path,
8 | joinPaths,
9 | endsWith,
10 | ) where
11 |
12 | import Appendable (Semigroup (..))
13 | import Array (Array)
14 | import Array qualified
15 | import Basics
16 | import Char (Char)
17 | import Control.Monad.Fail qualified as Monad
18 | import Json qualified
19 | import Language.Haskell.TH.Quote qualified as Quote
20 | import Language.Haskell.TH.Syntax (Lift)
21 | import Language.Haskell.TH.Syntax qualified as TH
22 | import LinkedList (LinkedList)
23 | import Maybe (Maybe (..))
24 | import Maybe qualified
25 | import Text (Text)
26 | import Text qualified
27 | import ToText qualified
28 |
29 |
30 | newtype Path = Path (LinkedList Char) -- We use LinkedList Char to keep compatibility with Haskell's FilePath type
31 | deriving (Lift, Eq, Ord, Json.FromJSON, Json.ToJSON)
32 |
33 |
34 | instance ToText.Show Path where
35 | show (Path p) = p
36 |
37 |
38 | instance IsString Path where
39 | fromString = Path
40 |
41 |
42 | instance Semigroup Path where
43 | (Path path1) <> (Path path2) = Path (path1 <> path2)
44 |
45 |
46 | fromText :: Text -> Maybe Path
47 | fromText text =
48 | -- FIXME: Implement proper path parsing by using
49 | -- a filepath parsing library
50 | Just (Path (Text.toLinkedList text))
51 |
52 |
53 | fromLinkedList :: LinkedList Char -> Maybe Path
54 | fromLinkedList list =
55 | Text.fromLinkedList list
56 | |> fromText
57 |
58 |
59 | -- | Smart text constructor to make a path from a text literal
60 | path :: Quote.QuasiQuoter
61 | path =
62 | Quote.QuasiQuoter
63 | { Quote.quoteExp = \text -> do
64 | case fromText (Text.fromLinkedList text) of
65 | Just p -> TH.lift p
66 | Nothing -> Monad.fail "Invalid path",
67 | Quote.quotePat = panic "path constructor can only be used in expressions",
68 | Quote.quoteType = panic "path constructor can only be used in expressions",
69 | Quote.quoteDec = panic "path constructor can only be used in expressions"
70 | }
71 |
72 |
73 | toText :: Path -> Text
74 | toText (Path linkedList) =
75 | Text.fromLinkedList linkedList
76 |
77 |
78 | toLinkedList :: Path -> LinkedList Char
79 | toLinkedList (Path linkedList) =
80 | linkedList
81 |
82 |
83 | -- | Joins paths in a cross-platform way
84 | -- TODO: Make this cross platform lol
85 | joinPaths :: Array Path -> Path
86 | joinPaths paths =
87 | paths
88 | |> Array.map toText
89 | |> Text.joinWith "/"
90 | |> fromText
91 | |> Maybe.getOrDie
92 |
93 |
94 | endsWith :: Text -> Path -> Bool
95 | endsWith txt self =
96 | Path.toText self
97 | |> Text.endsWith txt
98 |
--------------------------------------------------------------------------------
/core/system/Subprocess.hs:
--------------------------------------------------------------------------------
1 | module Subprocess (
2 | OpenOptions (..),
3 | Completion (..),
4 | InheritStream (..),
5 | open,
6 | openInherit,
7 | ) where
8 |
9 | import Array (Array)
10 | import Array qualified
11 | import Basics
12 | import Maybe qualified
13 | import Path (Path)
14 | import Path qualified
15 | import System.Exit qualified
16 | import System.Process qualified
17 | import Task (Task)
18 | import Task qualified
19 | import Text (Text)
20 | import Text qualified
21 | import ToText (Show)
22 |
23 |
24 | data Completion = Completion
25 | { exitCode :: Int,
26 | stdout :: Text,
27 | stderr :: Text
28 | }
29 | deriving (Eq, Ord, Show)
30 |
31 |
32 | data OpenOptions = OpenOptions
33 | { executable :: Text,
34 | arguments :: Array Text,
35 | directory :: Path
36 | }
37 | deriving (Eq, Ord, Show)
38 |
39 |
40 | data InheritStream
41 | = InheritSTDOUT
42 | | InheritSTDERR
43 | | InheritBOTH
44 | | InheritNONE
45 | deriving (Eq, Ord, Show)
46 |
47 |
48 | openInherit :: Text -> Array Text -> Path -> InheritStream -> Task _ Completion
49 | openInherit executable arguments directory inheritStream = do
50 | let (stdoutStream, stderrStream) = case inheritStream of
51 | InheritSTDOUT -> (System.Process.Inherit, System.Process.CreatePipe)
52 | InheritSTDERR -> (System.Process.CreatePipe, System.Process.Inherit)
53 | InheritBOTH -> (System.Process.Inherit, System.Process.Inherit)
54 | InheritNONE -> (System.Process.CreatePipe, System.Process.CreatePipe)
55 | let exec = Text.toLinkedList executable
56 | let args = Array.map Text.toLinkedList arguments |> Array.toLinkedList
57 | let processToExecute =
58 | ( System.Process.proc
59 | exec
60 | args
61 | )
62 | { System.Process.cwd =
63 | directory
64 | |> Path.toLinkedList
65 | |> Maybe.Just,
66 | System.Process.std_out = stdoutStream,
67 | System.Process.std_err = stderrStream
68 | }
69 | (_, _, _, ph) <-
70 | -- System.Process.readCreateProcessWithExitCode processToExecute ""
71 | System.Process.createProcess processToExecute
72 | |> Task.fromIO
73 | ec <- System.Process.waitForProcess ph |> Task.fromIO
74 | let exitCode = case ec of
75 | System.Exit.ExitSuccess -> 0
76 | System.Exit.ExitFailure code -> code
77 | let stdout = ""
78 | let stderr = ""
79 | Task.yield Completion {exitCode, stdout, stderr}
80 |
81 |
82 | open :: Text -> Array Text -> Path -> Task _ Completion
83 | open executable arguments directory =
84 | openInherit executable arguments directory InheritNONE
85 |
--------------------------------------------------------------------------------
/core/system/Time.hs:
--------------------------------------------------------------------------------
1 | module Time (triggerEveryMilliseconds) where
2 |
3 | import AsyncIO qualified
4 | import Basics
5 | import Nanotime qualified
6 | import Trigger (Trigger)
7 | import Trigger qualified
8 | import Prelude qualified
9 |
10 |
11 | -- | Create a trigger that generates an event every specified milliseconds.
12 | triggerEveryMilliseconds ::
13 | forall (event :: Type).
14 | Int ->
15 | (Int -> event) ->
16 | Trigger event
17 | triggerEveryMilliseconds milliseconds messageConstructor =
18 | Trigger.new \dispatch -> forever do
19 | posixTime <- Nanotime.currentTime @Nanotime.PosixTime
20 | let currentMs =
21 | posixTime
22 | |> Nanotime.unPosixTime
23 | |> Prelude.fromIntegral
24 | dispatch (messageConstructor currentMs)
25 | AsyncIO.sleep milliseconds
26 |
--------------------------------------------------------------------------------
/core/test/Main.hs:
--------------------------------------------------------------------------------
1 | module Main (main) where
2 |
3 | import Core
4 |
5 |
6 | main :: IO ()
7 | main = log "Test suite not yet implemented."
8 |
--------------------------------------------------------------------------------
/core/toml/Toml.hs:
--------------------------------------------------------------------------------
1 | module Toml (
2 | Decodable,
3 | Encodable,
4 | ) where
5 |
6 |
7 | -- TODO: implement: https://github.com/neohaskell/NeoHaskell/issues/116
8 | type Decodable value = value
9 |
10 |
11 | -- TODO: implement: https://github.com/neohaskell/NeoHaskell/issues/116
12 | type Encodable value = value
13 |
--------------------------------------------------------------------------------
/core/traits/Appendable.hs:
--------------------------------------------------------------------------------
1 | module Appendable (
2 | Appendable,
3 | Data.Semigroup.Semigroup (..),
4 | (++),
5 | ) where
6 |
7 | import Data.Semigroup qualified
8 |
9 |
10 | type Appendable = Data.Semigroup.Semigroup
11 |
12 |
13 | infixr 5 ++
14 |
15 |
16 | -- | Put two appendable things together. This includes arrays, lists, and text.
17 | --
18 | -- > "hello" ++ "world" == "helloworld"
19 | -- > [1,1,2] ++ [3,5,8] == [1,1,2,3,5,8]
20 | (++) :: (Appendable appendable) => appendable -> appendable -> appendable
21 | (++) =
22 | (Data.Semigroup.<>)
23 |
--------------------------------------------------------------------------------
/core/traits/Applicable.hs:
--------------------------------------------------------------------------------
1 | module Applicable (
2 | Applicable,
3 | Applicative (..),
4 | apply,
5 | ) where
6 |
7 | import Control.Applicative (Applicative (..))
8 |
9 |
10 | -- | The Applicable trait defines the behavior of a type that can be
11 | -- contains a function that can be applied to the contents of another type.
12 | --
13 | -- If you want to make a type applicable, you need to implement the
14 | -- `Applicative` trait.
15 | type Applicable applicable =
16 | Applicative applicable
17 |
18 |
19 | -- | The `apply` function applies a function to each element in a
20 | -- applicable value.
21 | apply ::
22 | (Applicable applicable) =>
23 | applicable (typeA -> typeB) ->
24 | applicable typeA ->
25 | applicable typeB
26 | apply = (<*>)
27 |
--------------------------------------------------------------------------------
/core/traits/Combinable.hs:
--------------------------------------------------------------------------------
1 | module Combinable (
2 | Combinable,
3 | Data.Monoid.Monoid (..),
4 | empty,
5 | ) where
6 |
7 | import Data.Monoid qualified
8 |
9 |
10 | type Combinable = Data.Monoid.Monoid
11 |
12 |
13 | -- | The empty combinable thing. This includes arrays, lists, and text.
14 | empty :: (Combinable combinable) => combinable
15 | empty =
16 | Data.Monoid.mempty
17 |
--------------------------------------------------------------------------------
/core/traits/Default.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE UndecidableInstances #-}
2 | {-# OPTIONS_GHC -Wno-orphans #-}
3 |
4 | module Default (Default (..), defaultValue) where
5 |
6 | import Char (Char)
7 | import Data.Default
8 | import Text (Text)
9 |
10 |
11 | defaultValue :: (Default value) => value
12 | defaultValue = def
13 |
14 |
15 | instance Default Char where
16 | def = '\0'
17 |
18 |
19 | instance Default Text where
20 | def = ""
21 |
--------------------------------------------------------------------------------
/core/traits/Mappable.hs:
--------------------------------------------------------------------------------
1 | module Mappable (
2 | Mappable,
3 | Functor (..),
4 | map,
5 | ) where
6 |
7 | import Data.Functor (Functor (..))
8 |
9 |
10 | -- | The Mappable trait defines the behavior of a type that can be
11 | -- mapped over.
12 | --
13 | -- If you want to make a type mappable, you need to implement the
14 | -- `Functor` trait.
15 | type Mappable mappable =
16 | Functor mappable
17 |
18 |
19 | -- | The `map` function applies a function to each element in a
20 | -- mappable value.
21 | map ::
22 | (Mappable mappable) =>
23 | (typeA -> typeB) ->
24 | mappable typeA ->
25 | mappable typeB
26 | map = fmap
27 |
--------------------------------------------------------------------------------
/core/traits/Thenable.hs:
--------------------------------------------------------------------------------
1 | module Thenable (
2 | Thenable,
3 | Monad (..),
4 | andThen,
5 | yield,
6 | ) where
7 |
8 | import Control.Monad (Monad (..), (=<<))
9 |
10 |
11 | -- | The `Thenable` trait defines that a type can be used in a
12 | -- chain of functions that return the same type.
13 | type Thenable thenable = Monad thenable
14 |
15 |
16 | -- | The `andThen` function chains two functions that return the same
17 | -- type.
18 | andThen ::
19 | (Thenable thenable) =>
20 | (a -> thenable b) ->
21 | thenable a ->
22 | thenable b
23 | andThen = (=<<)
24 |
25 |
26 | -- | The `yield` function creates a `Thenable` from a value.
27 | yield :: (Thenable thenable) => a -> thenable a
28 | yield = return
29 |
--------------------------------------------------------------------------------
/core/traits/ToText.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE UndecidableInstances #-}
2 |
3 | module ToText (
4 | Show (..),
5 | toPrettyText,
6 | ToText,
7 | toText,
8 | ) where
9 |
10 | import Basics
11 | import Data.Text.Lazy qualified as LazyText
12 | import Text (Text, fromLinkedList)
13 | import Text.Pretty.Simple qualified as PS
14 | import Prelude (Show (..))
15 |
16 |
17 | type ToText value = Show value
18 |
19 |
20 | toPrettyText :: (Show value) => value -> Text
21 | toPrettyText value =
22 | PS.pShow value
23 | |> LazyText.toStrict
24 |
25 |
26 | toText :: (Show value) => value -> Text
27 | toText value =
28 | show value
29 | |> Text.fromLinkedList
30 |
--------------------------------------------------------------------------------
/default.nix:
--------------------------------------------------------------------------------
1 | import ./cli/default.nix
2 |
--------------------------------------------------------------------------------
/fourmolu.yaml:
--------------------------------------------------------------------------------
1 | # Number of spaces per indentation step
2 | indentation: 2
3 |
4 | # Max line length for automatic line breaking
5 | column-limit: 120
6 |
7 | # Styling of arrows in type signatures (choices: trailing, leading, or leading-args)
8 | function-arrows: trailing
9 |
10 | # How to place commas in multi-line lists, records, etc. (choices: leading or trailing)
11 | comma-style: trailing
12 |
13 | # Styling of import/export lists (choices: leading, trailing, or diff-friendly)
14 | import-export-style: diff-friendly
15 |
16 | # Whether to full-indent or half-indent 'where' bindings past the preceding body
17 | indent-wheres: false
18 |
19 | # Whether to leave a space before an opening record brace
20 | record-brace-space: true
21 |
22 | # Number of spaces between top-level declarations
23 | newlines-between-decls: 2
24 |
25 | # How to print Haddock comments (choices: single-line, multi-line, or multi-line-compact)
26 | haddock-style: single-line
27 |
28 | # How to print module docstring
29 | haddock-style-module: null
30 |
31 | # Styling of let blocks (choices: auto, inline, newline, or mixed)
32 | let-style: inline
33 |
34 | # How to align the 'in' keyword with respect to the 'let' keyword (choices: left-align, right-align, or no-space)
35 | in-style: left-align
36 |
37 | # Whether to put parentheses around a single constraint (choices: auto, always, or never)
38 | single-constraint-parens: always
39 |
40 | # Output Unicode syntax (choices: detect, always, or never)
41 | unicode: never
42 |
43 | # Give the programmer more choice on where to insert blank lines
44 | respectful: false
45 |
46 | # Fixity information for operators
47 | fixities: []
48 |
49 | # Module reexports Fourmolu should know about
50 | reexports: []
51 |
--------------------------------------------------------------------------------
/nix/nixpkgs.nix:
--------------------------------------------------------------------------------
1 | import (builtins.fetchTarball {
2 | name = "haskell-fixes";
3 | url = "https://github.com/nixos/nixpkgs/archive/68bb0e401f249f1a78eb3ee840e4921d8db0c4b2.tar.gz";
4 | sha256 = "1wcpckkd2i9hxf27l80k35kv4s1jag7wikqj2y0kp7dmavqz7kn4";
5 | })
6 |
--------------------------------------------------------------------------------
/project.yaml:
--------------------------------------------------------------------------------
1 | # This is a test file that we're using for now to test the
2 | # build project that we're coding.
3 | name: foobar
4 | version: 1.0.0
5 |
6 |
--------------------------------------------------------------------------------
/sandbox/neo.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sandbox",
3 | "version": "1.0.0",
4 | "description": "An example neo.json file",
5 | "author": "Your Name",
6 | "license": "ISC",
7 | "dependencies": {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/sandbox/src/Sandbox.hs:
--------------------------------------------------------------------------------
1 | module Sandbox where
2 |
3 | import qualified Console
4 | import Core
5 |
6 |
7 | run :: Task Text ()
8 | run = do
9 | Console.print "Hello, NeoHaskell!"
10 |
--------------------------------------------------------------------------------
/scripts/install.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 |
5 | fail() {
6 | echo ""
7 | echo "neo: ❌ Oops! The installation script encountered an error."
8 | echo ""
9 | echo " If it's taking you more than 15 minutes to figure out,"
10 | echo " consider it a bug in the installer, not your fault."
11 | echo ""
12 | echo " Please report the issue at:"
13 | echo ""
14 | echo " https://github.com/neohaskell/neohaskell/issues/new"
15 | echo ""
16 | echo " Include your OS, shell, and anything you saw printed above."
17 | echo ""
18 | echo " I'll be waiting for you."
19 | exit 1
20 | }
21 |
22 | trap fail ERR
23 |
24 | if ! command -v nix &> /dev/null; then
25 | echo "⚙️ Nix is not installed. Installing Nix with Determinate Systems..."
26 | curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install --determinate
27 | . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
28 | fi
29 |
30 | echo "📦 Installing NeoHaskell from GitHub..."
31 | nix-env -if https://github.com/neohaskell/NeoHaskell/archive/refs/heads/main.tar.gz
32 |
33 | echo "✅ NeoHaskell installed successfully!"
34 | echo ""
35 | echo "Try running 'neo --help' to see what you can do with it."
36 |
--------------------------------------------------------------------------------
/scripts/run-doctest:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cabal install doctest --overwrite-policy=always && cabal build all && cabal repl --build-depends=QuickCheck --build-depends=template-haskell --with-ghc=doctest --repl-options='-w -Wdefault'
4 |
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | { pkgs ? import ./nix/nixpkgs.nix { } }:
2 | let
3 | nix-pre-commit-hooks = import (builtins.fetchTarball
4 | "https://github.com/cachix/git-hooks.nix/tarball/master");
5 | in let
6 | pre-commit = {
7 | # Configured with the module options defined in `modules/pre-commit.nix`:
8 | pre-commit-check = nix-pre-commit-hooks.run {
9 | src = ./.;
10 | # If your hooks are intrusive, avoid running on each commit with a default_states like this:
11 | # default_stages = ["manual" "pre-push"];
12 | hooks = { fourmolu.enable = true; };
13 | };
14 | };
15 | in pkgs.mkShell rec {
16 | buildInputs = [
17 | # Haskell dev tools
18 | pkgs.ghc
19 | pkgs.cabal-install
20 | pkgs.haskell-language-server
21 | pkgs.fourmolu
22 | pkgs.hlint
23 | pkgs.haskellPackages.zlib
24 |
25 | # Nix dev tools
26 | pkgs.nil
27 | pkgs.nixfmt-classic
28 | pkgs.nixpkgs-fmt
29 |
30 | # Required native libs
31 | pkgs.pkg-config
32 | pkgs.zlib
33 | ] ++ pre-commit.pre-commit-check.enabledPackages;
34 |
35 | shellHook = ''
36 | unset TEMP TMP TEMPDIR TMPDIR # Required for nix-shell to work
37 | ${pre-commit.pre-commit-check.shellHook}
38 | '';
39 |
40 | # Required for cabal to find the location of zlib and other native libraries
41 | LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs;
42 | }
43 |
--------------------------------------------------------------------------------