Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fix issues with parametricity. Collapse successive NonNulls. #26

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 40 additions & 2 deletions src/Data/Nullable.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,49 @@
"use strict";

// A None value is represented as null
exports["null"] = null;

// PRIVATE: Custom marker to detect wrapped nulls
var IS_WRAPPED_NULL = [];
// PRIVATE: Make a wrapped null
function WrappedNull(depth) {
return {isWrappedNull:IS_WRAPPED_NULL, depth: depth};
}
// PRIVATE: Check if this is a wrapped null
function isWrappedNull(x) {
return (x !== null && x.isWrappedNull === IS_WRAPPED_NULL);
}

exports.nullable = function (a, r, f) {
return a == null ? r : f(a);
// Unwrapped null
if (a == null) {
return r;
}

// By default, assume that this is a deeply nested non-null value
// Since we collapse non-null values, we can pass it directly to the function
var inner = a;

// If, instead this is a deeply wrapped null
if (isWrappedNull(a)) {
var depth = a.depth;
// Unwrap the Null entirely if only Singly wrapped
// Else unwrap one layer
inner = (depth <= 0)? null : WrappedNull(depth - 1| 0);
}

return f(inner);
};

exports.notNull = function (x) {
return x;
// Nulls get wrapped
if (x === null) {
return WrappedNull(0);
// Wrapped nulls, get wrapped even more
} else if (isWrappedNull(x)) {
return WrappedNull(x.depth + 1 | 0);
// We collapse non-null values into the value itself
} else {
return x;
}
};
19 changes: 1 addition & 18 deletions src/Data/Nullable.purs
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,6 @@ import Data.Ord (class Ord1)
-- | `null`, `[]`, and `[1,2,3]` may all be given the type
-- | `Nullable (Array Int)`. Similarly, the JavaScript values `[]`, `[null]`,
-- | and `[1,2,null,3]` may all be given the type `Array (Nullable Int)`.
-- |
-- | There is one pitfall with `Nullable`, which is that values of the type
-- | `Nullable T` will not function as you might expect if the type `T` happens
-- | to itself permit `null` as a valid runtime representation.
-- |
-- | In particular, values of the type `Nullable (Nullable T)` will ‘collapse’,
-- | in the sense that the PureScript expressions `notNull null` and `null`
-- | will both leave you with a value whose runtime representation is just
-- | `null`. Therefore it is important to avoid using `Nullable T` in
-- | situations where `T` itself can take `null` as a runtime representation.
-- | If in doubt, use `Maybe` instead.
-- |
-- | `Nullable` does not permit lawful `Functor`, `Applicative`, or `Monad`
-- | instances as a result of this pitfall, which is why these instances are
-- | not provided.
foreign import data Nullable :: Type -> Type

-- | The null value.
Expand All @@ -55,9 +40,7 @@ foreign import notNull :: forall a. a -> Nullable a
toNullable :: forall a. Maybe a -> Nullable a
toNullable = maybe null notNull

-- | Represent `null` using `Maybe a` as `Nothing`. Note that this function
-- | can violate parametricity, as it inspects the runtime representation of
-- | its argument (see the warning about the pitfall of `Nullable` above).
-- | Represent `null` using `Maybe a` as `Nothing`
toMaybe :: forall a. Nullable a -> Maybe a
toMaybe n = runFn3 nullable n Nothing Just

Expand Down
12 changes: 11 additions & 1 deletion test/Main.purs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module Test.Main where
import Prelude

import Data.Maybe (Maybe(..))
import Data.Nullable (toNullable, toMaybe)
import Data.Nullable (toNullable, toMaybe, null, Nullable)
import Effect (Effect)
import Test.Assert (assertEqual)

Expand All @@ -25,3 +25,13 @@ main = do
{ actual: toNullable Nothing `compare` toNullable (Just 42)
, expected: LT
}
assertEqual
{ actual: toMaybe (toNullable (Just 1)) == Just 1
, expected: true
}
-- Make sure we don't violate parametricity (See https://github.com/purescript-contrib/purescript-nullable/issues/7)
let (nullInt::Nullable Int) = null
assertEqual
{ actual: toMaybe (toNullable (Just nullInt)) == Just nullInt
, expected: true
}