diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..1c6afb9 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,26 @@ +{ + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + "strict": [2, "global"], + "block-scoped-var": 2, + "consistent-return": 2, + "eqeqeq": [2, "smart"], + "guard-for-in": 2, + "no-caller": 2, + "no-extend-native": 2, + "no-loop-func": 2, + "no-new": 2, + "no-param-reassign": 2, + "no-return-assign": 2, + "no-unused-expressions": 2, + "no-use-before-define": 2, + "radix": [2, "always"], + "indent": [2, 2], + "quotes": [2, "double"], + "semi": [2, "always"] + } +} diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..4435abb --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ +**Description of the change** + +Clearly and concisely describe the purpose of the pull request. If this PR relates to an existing issue or change proposal, please link to it. Include any other background context that would help reviewers understand the motivation for this PR. + +--- + +**Checklist:** + +- [ ] Added the change to the changelog's "Unreleased" section with a reference to this PR (e.g. "- Made a change (#0000)") +- [ ] Linked any existing issues or proposals that this pull request should close +- [ ] Updated or added relevant documentation +- [ ] Added a test for the contribution (if applicable) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c69237a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: purescript-contrib/setup-purescript@main + with: + purescript: "unstable" + + - uses: actions/setup-node@v2 + with: + node-version: "14.x" + + - name: Install dependencies + run: | + npm install -g bower + npm install + bower install --production + + - name: Build source + run: npm run-script build + + - name: Run tests + run: | + bower install + npm run-script test --if-present diff --git a/.gitignore b/.gitignore index 4d159c2..b846b63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ +/.* +!/.gitignore +!/.eslintrc.json +!/.github/ /bower_components/ /node_modules/ -/.pulp-cache/ /output/ -/.psci* -/src/.webpack.js +package-lock.json diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3475898..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: node_js -sudo: false -node_js: - - 0.10 -env: - - PATH=$HOME/purescript:$PATH -install: - - wget -O $HOME/purescript.tar.gz https://github.com/purescript/purescript/releases/download/v0.8.0-RC1/linux64.tar.gz - - tar -xvf $HOME/purescript.tar.gz -C $HOME/ - - chmod a+x $HOME/purescript - - npm install -script: - - npm run build diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b93cf9d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,68 @@ +# Changelog + +Notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +Breaking changes: + +New features: + +Bugfixes: + +Other improvements: + +## [v4.0.0](https://github.com/purescript/purescript-partial/releases/tag/v4.0.0) - 2022-04-27 + +Breaking changes: +- Migrate FFI to ES modules (#24 by @kl0tl and @JordanMartinez) + +New features: + +Bugfixes: + +Other improvements: + +## [v3.0.0](https://github.com/purescript/purescript-partial/releases/tag/v3.0.0) - 2021-02-26 + +Breaking changes: +- Added support for PureScript 0.14 and dropped support for all previous versions (#16) +- Removed deprecated `unsafePartialBecause` (#16) + +New features: + +Bugfixes: + +Other improvements: +- Added extra documentation to `unsafePartial` (#16) +- Removed outer function in `_crashWith` FFI (#16) +- Migrated CI to GitHub Actions and updated installation instructions to use Spago (#17) +- Added a changelog and pull request template (#18) + +## [v2.0.1](https://github.com/purescript/purescript-partial/releases/tag/v2.0.1) - 2019-02-04 + +- Added guide from documentation repo (@anttih) + +## [v2.0.0](https://github.com/purescript/purescript-partial/releases/tag/v2.0.0) - 2018-05-22 + +- Updated for PureScript 0.12 + +## [v1.2.0](https://github.com/purescript/purescript-partial/releases/tag/v1.2.0) - 2016-12-24 + +- Added `unsafePartialBecause` (@sharkdp) + +## [v1.1.2](https://github.com/purescript/purescript-partial/releases/tag/v1.1.2) - 2016-05-15 + +- Made a backwards-compatible minor fix for the upcoming PureScript 0.9 release. + +## [v1.1.1](https://github.com/purescript/purescript-partial/releases/tag/v1.1.1) - 2016-05-02 + +- Added a license in bower.json for Pursuit. + +## [v1.1.0](https://github.com/purescript/purescript-partial/releases/tag/v1.1.0) - 2015-12-18 + +- Added `crash`, `crashWith` and `unsafeCrashWith`. + +## [v1.0.0](https://github.com/purescript/purescript-partial/releases/tag/v1.0.0) - 2015-12-18 + +- Initial release diff --git a/LICENSE b/LICENSE index d3249fe..311379c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,20 +1,26 @@ -The MIT License (MIT) +Copyright 2018 PureScript -Copyright (c) 2015 PureScript +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +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. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +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. + +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. diff --git a/README.md b/README.md index ad2e711..5f1669d 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,225 @@ # purescript-partial -[![Latest release](http://img.shields.io/bower/v/purescript-partial.svg)](https://github.com/purescript/purescript-partial/releases) -[![Build Status](https://travis-ci.org/purescript/purescript-partial.svg?branch=master)](https://travis-ci.org/purescript/purescript-partial) +[![Latest release](http://img.shields.io/github/release/purescript/purescript-partial.svg)](https://github.com/purescript/purescript-partial/releases) +[![Build status](https://github.com/purescript/purescript-partial/workflows/CI/badge.svg?branch=master)](https://github.com/purescript/purescript-partial/actions?query=workflow%3ACI+branch%3Amaster) +[![Pursuit](https://pursuit.purescript.org/packages/purescript-partial/badge)](https://pursuit.purescript.org/packages/purescript-partial) Utilities for working with partial functions. ## Installation ``` -bower install purescript-partial +spago install partial ``` -## Module documentation +## Why have a Partial type class? -- [Partial.Unsafe](docs/Partial/Unsafe.md) +Every now and then, you will want to use _partial functions;_ that is, +functions which don't handle every possible case of their inputs. For example, +there is a function `fromJust :: ∀ a. Partial ⇒ Maybe a → a` in `Data.Maybe`, +which gives you the value inside a `Just` value, or throws an error if given +`Nothing`. + +It's important that types tell the truth wherever possible, because this is a +large part of what allows us to understand PureScript code easily and refactor +it fearlessly. However, in certain contexts, you know that e.g. an `Either` +value is always going to be `Right`, but you can't prove that to the type +checker, and so you want an escape hatch so that you can write a function that +doesn't have to deal with the `Left` case. This is often the case when +performance is important, for instance. + +Previously, partial functions have been indicated by putting the word "unsafe" +at the start of their names, or by putting them in an "Unsafe" module. For +instance, there was previously an `unsafeIndex` function in +`Data.Array.Unsafe`, and `fromJust` used to be in `Data.Maybe.Unsafe`. However, +this is not ideal, because the fact that these functions are partial, and +therefore unsafe if used carelessly, does not appear in the type. Consequently, +there is little to stop you from using it in an inappropriate manner by +accident. + +The Partial type class allows us to put this information back into the types, +and thereby allows us to clearly demarcate which parts of your code are +responsible for making sure that unsafe functions are used in a safe manner. + +## I just want to use a partial function, please + +If you try to just use a partial function, you'll most likely get an error +about no instance being found for the `Partial` class. Take this program, for +instance: + +```purescript +module Main where + +import Prelude +import Data.Maybe (Maybe(..), fromJust) +import Effect (Effect) +import Effect.Console (logShow) + +main :: Effect Unit +main = logShow (fromJust (Just 3)) +``` + +Because `fromJust` is partial, and because the partiality hasn't been +explicitly handled, you'll get an error: + +``` +at src/Main.purs line 8, column 1 - line 8, column 56 + + No type class instance was found for + + Prim.Partial +``` + +_Aside: Yes, this is not a fantastic error. It's going to get better soon._ + +The solution is usually to add an application of `unsafePartial` somewhere, +like this: + +```purescript +module Main where + +import Prelude +import Data.Maybe (Maybe(..), fromJust) +import Effect (Effect) +import Effect.Console (logShow) +import Partial.Unsafe (unsafePartial) + +main :: Effect Unit +main = logShow (unsafePartial (fromJust (Just 3))) +``` + +## Where should I put unsafePartial? + +The rule of thumb is to put `unsafePartial` at the level of your program such +that the types tell the truth, and the part of your program responsible for +making sure a use of a partial function is safe is also the part where the +`unsafePartial` is. This is perhaps best demonstrated with an example. + +Imagine that we want to represent vectors in 3D with an array containing +exactly 3 values (perhaps we want to use them with some other API that expects +this representation, and we don't want to be converting back and forth all the +time). In this case, we would usually use a `newtype` and avoid exporting the +constructor: + +```purescript +module Data.V3 + ( V3() + , makeV3 + , runV3 + ) where + +newtype V3 = V3 (Array Number) + +makeV3 :: Number -> Number -> Number -> V3 +makeV3 x y z = V3 [x, y, z] + +runV3 :: V3 -> Array Number +runV3 (V3 v) = v +``` + +This way, all of the functions are safe; the code will guarantee that any `V3` +does contain exactly 3 values (although the type checker is not aware of this). + +Now imagine we want to write a dot product function: + +```purescript +dot :: V3 -> V3 -> Number +dot (V3 [x1, x2, x3]) (V3 [y1, y2, y3]) = x1*y1 + x2*y2 + x3*y3 +``` + +We know this is ok, but the compiler disallows it: + +``` +A case expression could not be determined to cover all inputs. +The following additional cases are required to cover all inputs: + + (V3 _) _ + _ (V3 _) + +Alternatively, add a Partial constraint to the type of the enclosing value. + +in value declaration dot +``` + +In this case, we can use `unsafePartial` to explicitly say that we don't +actually need to worry about those other cases, and therefore we don't want to +propagate a `Partial` constraint; users of this `dot` function should not have +to worry about this partiality. For example: + +```purescript +dot :: V3 -> V3 -> Number +dot x y = Partial.Unsafe.unsafePartial (go x y) + where + go :: Partial => V3 -> V3 -> Number + go (V3 [x1, x2, x3]) (V3 [y1, y2, y3]) = x1*y1 + x2*y2 + x3*y3 + -- This second pattern can be omitted, but provides a better error message + -- in case we do get an invalid argument at runtime. + go _ _ = Partial.crash "Bad argument: expected exactly 3 elements." +``` + +The `unsafePartial` function comes from the `Partial.Unsafe` module, in the +`purescript-partial` package. + +In this case, we could also use `Partial.Unsafe.unsafeCrashWith`: + +```purescript +dot :: V3 -> V3 -> Number +dot (V3 [x1, x2, x3]) (V3 [y1, y2, y3]) = x1*y1 + x2*y2 + x3*y3 +dot _ _ = unsafeCrashWith "Bad argument: expected exactly 3 elements." +``` + +Both implementations will behave in the same way. + +In this case, we know our `dot` implementation is fine, and so users of it +should not have to worry about its partiality, so it makes sense to avoid +propagating the constraint. Now, we will see another case where a `Partial` +constraint _should_ be propagated. + +Let us suppose we want a `foldr1` function, which works in a very similar way +to `foldr` on Lists, except that it doesn't require an initial value to be +passed, and instead requires that the list argument contains at least one +element. + +We can implement it like this: + +```purescript +foldr1 f (Cons x xs) = foldr f x xs +``` + +The compiler infers the correct type here, which is: + +```purescript +foldr1 :: forall a. Partial => (a -> a -> a) -> List a -> a +``` + +Now imagine we want a version of `Data.Foldable.minimum` which returns an `a` +instead of a `Maybe a`, and is therefore partial. We can implement it in terms +of our new `foldr1` function: + +```purescript +minimumP = foldr1 min +``` + +Again, the compiler infers the correct type: + +```purescript +minimumP :: forall a. Partial => Ord a => List a -> a +``` + +Notice that the `Partial` constraint is automatically propagated to the +`minimumP` function because of the use of another partial function in its +definition, namely `foldr1`. In this case, this is what we want; we should +propagate the `Partial` constraint, because it is still the caller's +responsibility to make sure they supply a non-empty list. + +So hopefully it is now clear why this partiality checking is implemented in +terms of a type class: it allows us to elegantly reuse existing machinery in +the type checker in order to check that a Partial constraint is either +explictly handled or propagated. This should help ensure that when you're +reading the code a few months later, it remains clear which part of the code is +responsible for ensuring that any assumed invariants which cannot be encoded in +the type system do hold. + +## API Documentation + +- API documentation is [published on Pursuit](http://pursuit.purescript.org/packages/purescript-partial). diff --git a/bower.json b/bower.json index c483c80..64a4e15 100644 --- a/bower.json +++ b/bower.json @@ -1,16 +1,19 @@ { "name": "purescript-partial", - "moduleType": [ - "node" - ], + "homepage": "https://github.com/purescript/purescript-partial", + "description": "Utilities for working with partial functions", + "license": "BSD-3-Clause", + "repository": { + "type": "git", + "url": "https://github.com/purescript/purescript-partial.git" + }, "ignore": [ "**/.*", - "node_modules", "bower_components", - "output" - ], - "repository": { - "type": "git", - "url": "git://github.com/purescript/purescript-partial.git" - } + "node_modules", + "output", + "test", + "bower.json", + "package.json" + ] } diff --git a/docs/Partial.md b/docs/Partial.md deleted file mode 100644 index 8fd3a12..0000000 --- a/docs/Partial.md +++ /dev/null @@ -1,21 +0,0 @@ -## Module Partial - -Some partial helper functions. - -#### `crash` - -``` purescript -crash :: forall a. (Partial) => a -``` - -A partial function which crashes on any input with a default message. - -#### `crashWith` - -``` purescript -crashWith :: forall a. (Partial) => String -> a -``` - -A partial function which crashes on any input with the specified message. - - diff --git a/docs/Partial/Unsafe.md b/docs/Partial/Unsafe.md deleted file mode 100644 index 3c9a5eb..0000000 --- a/docs/Partial/Unsafe.md +++ /dev/null @@ -1,21 +0,0 @@ -## Module Partial.Unsafe - -Utilities for working with partial functions. - -#### `unsafePartial` - -``` purescript -unsafePartial :: forall a. (Partial => a) -> a -``` - -Discharge a partiality constraint, unsafely. - -#### `unsafeCrashWith` - -``` purescript -unsafeCrashWith :: forall a. String -> a -``` - -A function which crashes with the specified error message. - - diff --git a/package.json b/package.json index e3348e6..a1d6811 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { "private": true, "scripts": { - "postinstall": "pulp dep install", - "build": "pulp build && rimraf docs && pulp docs" + "clean": "rimraf output && rimraf .pulp-cache", + "build": "eslint src && pulp build -- --censor-lib --strict", + "test": "pulp test" }, "devDependencies": { - "pulp": "^6.0.0", - "rimraf": "^2.4.1" + "eslint": "^7.15.0", + "pulp": "16.0.0-0", + "purescript-psa": "^0.8.2", + "rimraf": "^3.0.2" } } diff --git a/src/Partial.js b/src/Partial.js index bfac946..1fea9b6 100644 --- a/src/Partial.js +++ b/src/Partial.js @@ -1,9 +1,5 @@ -"use strict"; - // module Partial -exports.crashWith = function() { - return function(msg) { - throw new Error(msg); - }; +export const _crashWith = function (msg) { + throw new Error(msg); }; diff --git a/src/Partial.purs b/src/Partial.purs index 3943f4f..22e2b07 100644 --- a/src/Partial.purs +++ b/src/Partial.purs @@ -1,5 +1,5 @@ --- | Some partial helper functions. -module Partial +-- | Some partial helper functions. See the README for more documentation. +module Partial ( crash , crashWith ) where @@ -9,4 +9,7 @@ crash :: forall a. Partial => a crash = crashWith "Partial.crash: partial function" -- | A partial function which crashes on any input with the specified message. -foreign import crashWith :: forall a. Partial => String -> a +crashWith :: forall a. Partial => String -> a +crashWith = _crashWith + +foreign import _crashWith :: forall a. String -> a diff --git a/src/Partial/Unsafe.js b/src/Partial/Unsafe.js index 73f7379..de0cb0e 100644 --- a/src/Partial/Unsafe.js +++ b/src/Partial/Unsafe.js @@ -1,7 +1,5 @@ -"use strict"; - // module Partial.Unsafe -exports.unsafePartial = function(f) { - return f(); +export const _unsafePartial = function (f) { + return f(); }; diff --git a/src/Partial/Unsafe.purs b/src/Partial/Unsafe.purs index 037e375..2221d09 100644 --- a/src/Partial/Unsafe.purs +++ b/src/Partial/Unsafe.purs @@ -1,12 +1,24 @@ -- | Utilities for working with partial functions. -module Partial.Unsafe +-- | See the README for more documentation. +module Partial.Unsafe ( unsafePartial , unsafeCrashWith ) where +import Partial (crashWith) + +-- Note: this function's type signature is more like +-- `(Unit -> a) -> a`. However, we would need to use +-- `unsafeCoerce` to make this compile, incurring +-- either a dependency or reimplementing it here. +-- Rather than doing that, we'll use a type signature +-- of `a -> b` instead. +foreign import _unsafePartial :: forall a b. a -> b + -- | Discharge a partiality constraint, unsafely. -foreign import unsafePartial :: forall a. (Partial => a) -> a +unsafePartial :: forall a. (Partial => a) -> a +unsafePartial = _unsafePartial -- | A function which crashes with the specified error message. unsafeCrashWith :: forall a. String -> a -unsafeCrashWith msg = unsafePartial (Partial.crashWith msg) +unsafeCrashWith msg = unsafePartial (crashWith msg) diff --git a/test/Main.purs b/test/Main.purs index fb6524e..837e6fb 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -1,11 +1,14 @@ module Test.Main where +import Partial (crashWith) +import Partial.Unsafe (unsafePartial) + f :: Partial => Int -> Int f 0 = 0 -f _ = Partial.crashWith "f: partial function" +f _ = crashWith "f: partial function" safely :: Int -safely = Partial.Unsafe.unsafePartial (f 0) +safely = unsafePartial (f 0) main :: forall a. a -> {} main _ = {}