Skip to content

#1637, add a typeApplications option for restricted functions - #1699

Merged
zliu41 merged 1 commit into
ndmitchell:masterfrom
NorfairKing:require-type-applications
Sep 16, 2026
Merged

zliu41 merged 1 commit into
ndmitchell:masterfrom
NorfairKing:require-type-applications

Conversation

@NorfairKing

@NorfairKing NorfairKing commented Jun 28, 2026 •

Copy link
Copy Markdown
Contributor

Revives #1667 (closed stale), addressing #1637.

Function restrictions gain a typeApplications field, so a silent change to an inferred type can't change behaviour unnoticed:

- functions:
  - {name: show, typeApplications: required}     # >= 1 type argument
  - {name: fromIntegral, typeApplications: 2}    # >= N type arguments
  - {name: id, typeApplications: forbidden}      # none

Also applies to constructors in prefix patterns (Just @Int x).

Semantics

  • Only positions that can carry a type application are checked: expression heads and prefix constructor patterns. Type signatures, class method signatures, record fields, binders, infix operators and sections are never flagged, since the advice couldn't be followed there.
  • @_ doesn't count towards required, since it leaves the type as inferred as writing nothing — fromIntegral @_ @_ is still flagged. It does count towards forbidden.
  • When several rules match one name the last-declared wins. Merging is no use: a name can't both require and forbid a type application.

Testing

hlint --test passes. tests/type_applications.test covers required/forbidden/count, constructor patterns, each non-call position above, wildcards in both modes, and conflicting rules in both declaration orders.

Changes since review

Per @zliu41's review:

  • the check no longer fires in positions that can't carry a type application (type signatures, class method signatures, record fields, binders, infix operators, sections)
  • @_ no longer counts towards required, so fromIntegral @_ @_ is flagged
  • overlapping rules resolve by last-declared, rather than an arbitrary merge
  • tests for each of the above, including the qualified/unqualified fromList case in both declaration orders

Written by Claude, reviewed and signed-off on by @NorfairKing

@NorfairKing

Copy link
Copy Markdown
Contributor Author

Gentle ping. How would I go about getting this merged?

@NorfairKing

Copy link
Copy Markdown
Contributor Author

CC @ndmitchell I'm not sure who to ping.

@zliu41 zliu41 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A name can appear in different places like declarations, patterns, type signatures etc. Need to test that the hint is only triggered when it should - the current tests aren't sufficient.

{name: fromIntegral, typeApplications: 2}

Does this allow fromIntegerl @_ @_? Either way, need test coverage.

- {name: Just, typeApplications: required}
- {name: Left, typeApplications: forbidden}
- {name: Data.Map.fromList, typeApplications: required}
- {name: Data.Set.fromList, typeApplications: forbidden}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What happens if one of the two fromList is unqualified?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Both rules matched and the merge decided it, so declaration order had no effect at all. Same root cause as your Semigroup comment, so it's fixed the same way: the last-declared rule now wins.

With Data.Map.fromList: required then fromList: forbidden, an unqualified fromList from Data.Map ends up forbidden from carrying one; swap the order and it's required. Both directions are pinned in tests/type_applications.test, along with the qualified-vs-qualified pair.

Comment thread src/Config/Type.hs Outdated
TypeAppRequired a <> TypeAppRequired b = TypeAppRequired (max a b)
TypeAppForbidden <> TypeAppForbidden = TypeAppForbidden
-- If a function is both required and forbidden to carry a type application
-- (e.g. via overlapping rules), requiring one wins.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is quite arbitrary. How about whichever rule declared later wins?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and done.

It needed a little more than swapping the operator: restrictions builds a Map and findFunction sconcats Map.elems, so by merge time the declaration order was gone entirely. Rules now carry their position in the settings and the payload is Maybe (Max (Arg Int RestrictTypeApp)), so the latest wins across both the same-key union and the cross-module sconcat. The ad-hoc Semigroup RestrictTypeApp is gone. Tested in both declaration orders.

@NorfairKing
NorfairKing force-pushed the require-type-applications branch from 6acab78 to cbd3c6a Compare August 3, 2026 10:05
@NorfairKing
NorfairKing marked this pull request as draft August 3, 2026 10:58
@NorfairKing
NorfairKing marked this pull request as ready for review August 3, 2026 18:38
@NorfairKing

Copy link
Copy Markdown
Contributor Author

Thanks, both were real.

Non-call positions. You were right that the tests weren't sufficient. It was firing on top-level type signatures, class method signatures, record field declarations and argument binders, and on a further sweep also on infix operators, sections, infix constructor patterns and record-syntax constructor patterns. None of those can carry a type application, so the advice couldn't be followed. The check now works off a positive set of sites — expression heads and prefix constructor patterns — instead of every RdrName in the declaration, and tests/type_applications.test has a case per position.

One case I left firing and documented rather than fixed: a locally bound name shadowing a restricted one, since HLint doesn't resolve local binders. That matches the existing Avoid restricted function behaviour.

fromIntegral @_ @_. It did pass, which defeats the point of #1637 since @_ fixes nothing. It no longer counts towards required, so that's now flagged. It still counts towards forbidden, which asks that no visible type application is written at all. Tests for @_ @_, @Int @_ and @Int @Integer under 2, and @_ under forbidden.

@NorfairKing
NorfairKing requested a review from zliu41 August 3, 2026 19:05
@NorfairKing

Copy link
Copy Markdown
Contributor Author

@tomjaguarpaw this is the implementation of the haskell/core-libraries-committee#314; which you asked to be CC-ed on.

@NorfairKing

Copy link
Copy Markdown
Contributor Author

Gentle ping

@zliu41

zliu41 commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Please resolve conflicts

@NorfairKing
NorfairKing force-pushed the require-type-applications branch from 172e096 to 1601ad0 Compare September 16, 2026 12:23
Reviving ndmitchell#1667 (closed stale). A function restriction may now set
'typeApplications' to require or forbid visible type applications at use sites,
so a silent change to an inferred type cannot change behaviour unnoticed.

  - functions:
    - {name: show, typeApplications: required}     # >= 1 type argument
    - {name: fromIntegral, typeApplications: 2}    # >= N type arguments
    - {name: id, typeApplications: forbidden}      # none

'required' demands at least one type argument, an integer N demands at least N
(so fromIntegral, which needs both type variables fixed, can require two), and
'forbidden' demands none. It also applies to data constructors in patterns.

Only positions that can carry a visible type application are checked: the head
of an expression and a constructor in a prefix pattern. A name in a type
signature, a class method signature, a record field declaration or a binder is
never flagged, and neither is an operator used infix or in a section, or a
constructor pattern in record or infix form, since the advice cannot be
followed there. Parenthesised, as in (<+>), the operator is back in prefix
position and is still checked.

A '@_' argument does not count towards 'required', since it leaves the type
just as inferred as writing no type argument at all, so 'fromIntegral @_ @_' is
flagged under 'typeApplications: 2'. It does count towards 'forbidden', which
asks that no visible type application is written at all.

When several rules restrict one name's type applications, the last-declared one
wins. Merging them would be arbitrary: a name cannot sensibly both require and
forbid a type application.
@NorfairKing
NorfairKing force-pushed the require-type-applications branch from 1601ad0 to 1ddb17c Compare September 16, 2026 12:25
@zliu41
zliu41 merged commit 15d11e9 into ndmitchell:master Sep 16, 2026
0 of 5 checks passed
@NorfairKing

Copy link
Copy Markdown
Contributor Author

❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants